2024-04-02 22:24:17 -03:00
|
|
|
/*
|
2026-03-17 08:38:25 -03:00
|
|
|
* Copyright (c) 2024-2026, Tim Flynn <trflynn89@ladybird.org>
|
2024-04-28 11:07:23 -03:00
|
|
|
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
|
2024-07-26 07:38:26 -03:00
|
|
|
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
|
2024-04-02 22:24:17 -03:00
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2024-04-02 22:38:01 -03:00
|
|
|
#include <AK/StdLibExtras.h>
|
2026-05-25 16:22:07 -03:00
|
|
|
#include <UI/Qt/Application.h>
|
|
|
|
|
#include <UI/Qt/BrowserWindow.h>
|
2026-06-02 12:45:44 -03:00
|
|
|
#include <UI/Qt/ChromeLayout.h>
|
2026-05-25 16:21:50 -03:00
|
|
|
#include <UI/Qt/ChromeStyle.h>
|
2026-03-17 08:38:25 -03:00
|
|
|
#include <UI/Qt/Icon.h>
|
2026-06-02 12:45:44 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
# include <UI/Qt/MacWindow.h>
|
|
|
|
|
#endif
|
2026-05-31 19:35:03 -03:00
|
|
|
#include <UI/Qt/Menu.h>
|
2026-06-10 21:26:41 -03:00
|
|
|
#include <UI/Qt/StringUtils.h>
|
2024-11-09 14:50:33 -03:00
|
|
|
#include <UI/Qt/Tab.h>
|
|
|
|
|
#include <UI/Qt/TabBar.h>
|
2026-05-27 13:16:16 -03:00
|
|
|
#include <UI/Qt/WindowControlButton.h>
|
2024-11-09 14:50:33 -03:00
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
#include <QApplication>
|
2024-04-28 11:07:23 -03:00
|
|
|
#include <QContextMenuEvent>
|
2026-05-25 16:22:07 -03:00
|
|
|
#include <QCursor>
|
|
|
|
|
#include <QDrag>
|
|
|
|
|
#include <QDragEnterEvent>
|
|
|
|
|
#include <QDragLeaveEvent>
|
|
|
|
|
#include <QDragMoveEvent>
|
|
|
|
|
#include <QDropEvent>
|
2026-05-25 16:22:32 -03:00
|
|
|
#include <QEasingCurve>
|
2024-04-02 22:24:17 -03:00
|
|
|
#include <QEvent>
|
2026-05-25 16:21:50 -03:00
|
|
|
#include <QFontMetrics>
|
2026-06-10 21:26:41 -03:00
|
|
|
#include <QFrame>
|
|
|
|
|
#include <QGraphicsDropShadowEffect>
|
2026-03-17 08:38:25 -03:00
|
|
|
#include <QHBoxLayout>
|
2026-06-10 21:26:41 -03:00
|
|
|
#include <QHelpEvent>
|
|
|
|
|
#include <QLabel>
|
2026-05-31 14:17:47 -03:00
|
|
|
#include <QLayoutItem>
|
2026-05-25 16:22:32 -03:00
|
|
|
#include <QLinearGradient>
|
2026-05-25 16:22:07 -03:00
|
|
|
#include <QMimeData>
|
2026-03-17 08:38:25 -03:00
|
|
|
#include <QMouseEvent>
|
2026-05-25 16:21:50 -03:00
|
|
|
#include <QPaintEvent>
|
2026-05-19 16:29:50 -03:00
|
|
|
#include <QPainter>
|
2026-05-25 16:21:50 -03:00
|
|
|
#include <QPainterPath>
|
2026-05-25 16:22:07 -03:00
|
|
|
#include <QPixmap>
|
2026-06-10 21:26:41 -03:00
|
|
|
#include <QScreen>
|
2026-05-31 14:17:47 -03:00
|
|
|
#include <QStyle>
|
2026-06-01 10:46:39 -03:00
|
|
|
#include <QTimer>
|
2026-03-17 08:38:25 -03:00
|
|
|
#include <QToolButton>
|
2026-06-10 21:26:41 -03:00
|
|
|
#include <QToolTip>
|
2026-03-17 08:38:25 -03:00
|
|
|
#include <QVBoxLayout>
|
2026-06-02 15:34:12 -03:00
|
|
|
#include <QVariant>
|
2026-05-25 16:22:32 -03:00
|
|
|
#include <QVariantAnimation>
|
2026-05-31 14:17:47 -03:00
|
|
|
#include <QWheelEvent>
|
2026-05-25 16:21:50 -03:00
|
|
|
#include <QWindow>
|
2024-04-02 22:24:17 -03:00
|
|
|
|
|
|
|
|
namespace Ladybird {
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
static constexpr auto LADYBIRD_TAB_MIME_TYPE = "application/x-ladybird-tab";
|
2026-06-02 12:45:44 -03:00
|
|
|
static constexpr int HORIZONTAL_TAB_STRIP_HEIGHT = 44;
|
|
|
|
|
static constexpr int HORIZONTAL_TAB_HEIGHT = 38;
|
2026-06-01 08:41:54 -03:00
|
|
|
static constexpr int HORIZONTAL_TAB_MIN_WIDTH = 128;
|
|
|
|
|
static constexpr int HORIZONTAL_TAB_MAX_WIDTH = 240;
|
2026-05-31 14:17:47 -03:00
|
|
|
static constexpr int VERTICAL_TAB_HEIGHT = 38;
|
2026-06-02 12:45:44 -03:00
|
|
|
static constexpr int VERTICAL_TABS_COLLAPSED_WIDTH = browser_chrome_layout_policy().collapsed_sidebar_width;
|
|
|
|
|
static constexpr int VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH = browser_chrome_layout_policy().expanded_sidebar_width;
|
2026-05-31 19:37:00 -03:00
|
|
|
static constexpr int VERTICAL_TABS_MIN_EXPANDED_WIDTH = 190;
|
|
|
|
|
static constexpr int VERTICAL_TABS_MAX_EXPANDED_WIDTH = 400;
|
|
|
|
|
static constexpr int VERTICAL_TABS_RESIZE_HIT_AREA_WIDTH = 5;
|
2026-06-01 06:09:45 -03:00
|
|
|
static constexpr int VERTICAL_TABS_COLLAPSED_SIDE_MARGIN = 6;
|
|
|
|
|
static constexpr int VERTICAL_TABS_EXPANDED_SIDE_MARGIN = 5;
|
2026-06-02 12:45:44 -03:00
|
|
|
static constexpr int VERTICAL_TABS_TOP_MARGIN = 8;
|
|
|
|
|
static constexpr int TAB_CARD_SHAPE_HORIZONTAL_INSET = 5;
|
2026-06-02 16:24:13 -03:00
|
|
|
static constexpr qreal HORIZONTAL_TAB_CARD_SHAPE_HORIZONTAL_INSET = 4.0;
|
2026-06-02 12:45:44 -03:00
|
|
|
static constexpr int TAB_CARD_SHAPE_VERTICAL_INSET = 3;
|
2026-06-02 16:24:13 -03:00
|
|
|
static constexpr int HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE = 32;
|
2026-06-02 16:17:36 -03:00
|
|
|
static constexpr int TAB_CONTENT_HORIZONTAL_INSET = 8;
|
2026-06-01 10:46:39 -03:00
|
|
|
static constexpr int VERTICAL_TABS_HOVER_COLLAPSE_POLL_INTERVAL_MS = 250;
|
2026-06-10 21:26:41 -03:00
|
|
|
static constexpr int TAB_PREVIEW_HOVER_DELAY_MS = 350;
|
|
|
|
|
static constexpr int TAB_PREVIEW_THUMBNAIL_WIDTH = 320;
|
|
|
|
|
static constexpr int TAB_PREVIEW_THUMBNAIL_HEIGHT = 180;
|
|
|
|
|
static constexpr int TAB_PREVIEW_SHADOW_MARGIN = 12;
|
|
|
|
|
static constexpr int TAB_PREVIEW_CARD_GAP = 6;
|
2026-06-02 11:21:08 -03:00
|
|
|
static constexpr int TAB_BUTTON_SIZE = 22;
|
2026-05-31 14:17:47 -03:00
|
|
|
static constexpr int TAB_ICON_SIZE = 16;
|
2026-06-02 11:21:08 -03:00
|
|
|
static constexpr int COLLAPSED_VERTICAL_TAB_BUTTON_SIZE = 16;
|
|
|
|
|
static constexpr int COLLAPSED_VERTICAL_TAB_ICON_SIZE = 12;
|
|
|
|
|
static constexpr auto COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY = "collapsedVerticalTabButton";
|
2026-06-02 12:45:44 -03:00
|
|
|
static constexpr auto FULL_WIDTH_TOOLBAR_PROPERTY = "fullWidthToolbar";
|
|
|
|
|
static constexpr auto VERTICAL_TABS_EXPANDED_PROPERTY = "verticalTabsExpanded";
|
|
|
|
|
static constexpr auto VERTICAL_TABS_BUTTON_PROPERTY = "verticalTabsButton";
|
2026-05-31 19:37:00 -03:00
|
|
|
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY = "hovered";
|
|
|
|
|
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY = "active";
|
2026-05-25 16:22:07 -03:00
|
|
|
|
2026-06-02 15:34:12 -03:00
|
|
|
static void set_dynamic_property_if_needed(QWidget& widget, char const* property, QVariant const& value)
|
|
|
|
|
{
|
|
|
|
|
if (widget.property(property) == value)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
widget.setProperty(property, value);
|
|
|
|
|
widget.style()->unpolish(&widget);
|
|
|
|
|
widget.style()->polish(&widget);
|
|
|
|
|
widget.update();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
static QPointer<TabWidget> s_active_tab_drag_source;
|
|
|
|
|
static QPointer<Tab> s_active_tab_dragged_tab;
|
|
|
|
|
static QPointer<TabWidget> s_pending_tab_drop_target;
|
|
|
|
|
static int s_pending_tab_drop_index { -1 };
|
|
|
|
|
|
2026-05-29 10:57:15 -03:00
|
|
|
static bool window_uses_client_side_decorations(QWidget& widget)
|
|
|
|
|
{
|
|
|
|
|
auto* top_level_window = qobject_cast<BrowserWindow*>(widget.window());
|
|
|
|
|
return !top_level_window || top_level_window->uses_client_side_decorations();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:32 -03:00
|
|
|
static QPainterPath tab_shape_path(QRectF const& rect, qreal top_radius, qreal bottom_radius)
|
2026-05-25 16:21:50 -03:00
|
|
|
{
|
2026-05-25 16:22:32 -03:00
|
|
|
top_radius = min(top_radius, rect.height() / 2.0);
|
|
|
|
|
bottom_radius = min(bottom_radius, rect.height() / 2.0);
|
2026-05-25 16:22:07 -03:00
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
QPainterPath path;
|
2026-05-25 16:22:32 -03:00
|
|
|
path.moveTo(rect.left() + top_radius, rect.top());
|
|
|
|
|
path.lineTo(rect.right() - top_radius, rect.top());
|
|
|
|
|
path.quadTo(rect.right(), rect.top(), rect.right(), rect.top() + top_radius);
|
|
|
|
|
path.lineTo(rect.right(), rect.bottom() - bottom_radius);
|
|
|
|
|
path.quadTo(rect.right(), rect.bottom(), rect.right() - bottom_radius, rect.bottom());
|
|
|
|
|
path.lineTo(rect.left() + bottom_radius, rect.bottom());
|
|
|
|
|
path.quadTo(rect.left(), rect.bottom(), rect.left(), rect.bottom() - bottom_radius);
|
|
|
|
|
path.lineTo(rect.left(), rect.top() + top_radius);
|
|
|
|
|
path.quadTo(rect.left(), rect.top(), rect.left() + top_radius, rect.top());
|
2026-05-25 16:21:50 -03:00
|
|
|
path.closeSubpath();
|
|
|
|
|
return path;
|
|
|
|
|
}
|
2026-05-19 16:29:50 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
static void clear_layout(QLayout& layout)
|
|
|
|
|
{
|
|
|
|
|
while (auto* item = layout.takeAt(0))
|
|
|
|
|
delete item;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 06:09:45 -03:00
|
|
|
static constexpr int vertical_tabs_side_margin(bool expanded)
|
|
|
|
|
{
|
|
|
|
|
return expanded ? VERTICAL_TABS_EXPANDED_SIDE_MARGIN : VERTICAL_TABS_COLLAPSED_SIDE_MARGIN;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
static constexpr int vertical_tabs_horizontal_margin_width(TabLayout tab_layout)
|
|
|
|
|
{
|
2026-06-02 15:22:48 -03:00
|
|
|
return vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed) * 2;
|
2026-06-02 12:45:44 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
static constexpr int vertical_tab_width(int available_width, TabLayout tab_layout)
|
|
|
|
|
{
|
|
|
|
|
if (available_width > 0)
|
|
|
|
|
return available_width;
|
2026-05-31 19:37:00 -03:00
|
|
|
auto width = tab_layout == TabLayout::VerticalCollapsed ? VERTICAL_TABS_COLLAPSED_WIDTH : VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH;
|
2026-06-02 12:45:44 -03:00
|
|
|
return width - vertical_tabs_horizontal_margin_width(tab_layout);
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
static constexpr int clamp_vertical_tabs_expanded_width(int width)
|
|
|
|
|
{
|
|
|
|
|
return clamp(width, VERTICAL_TABS_MIN_EXPANDED_WIDTH, VERTICAL_TABS_MAX_EXPANDED_WIDTH);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
static QRectF tab_card_shape_rect(QRectF const& rect)
|
|
|
|
|
{
|
|
|
|
|
return rect.adjusted(TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 16:17:36 -03:00
|
|
|
static QRectF horizontal_tab_card_shape_rect(QRectF const& rect)
|
|
|
|
|
{
|
|
|
|
|
return rect.adjusted(HORIZONTAL_TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -HORIZONTAL_TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 16:24:13 -03:00
|
|
|
static QRectF horizontal_new_tab_button_shape_rect(QRectF const& rect)
|
|
|
|
|
{
|
|
|
|
|
auto x = rect.left() + (rect.width() - HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE) / 2.0;
|
|
|
|
|
auto y = rect.top() + (rect.height() - HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE) / 2.0;
|
|
|
|
|
return { x, y, HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE, HORIZONTAL_NEW_TAB_BUTTON_SHAPE_SIZE };
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
static QRect tab_card_shape_rect(QRect const& rect)
|
2026-06-01 06:09:45 -03:00
|
|
|
{
|
2026-06-02 12:45:44 -03:00
|
|
|
return rect.adjusted(TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QColor tab_hover_surface(QPalette const& palette, qreal hover_progress)
|
|
|
|
|
{
|
|
|
|
|
auto dark = ChromeStyle::is_dark(palette);
|
|
|
|
|
auto color = dark ? QColor(255, 255, 255) : QColor(0, 0, 0);
|
|
|
|
|
color.setAlpha(static_cast<int>((dark ? 24 : 16) * hover_progress));
|
|
|
|
|
return color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QColor selected_tab_border(QPalette const& palette, bool collapsed)
|
|
|
|
|
{
|
|
|
|
|
auto dark = ChromeStyle::is_dark(palette);
|
|
|
|
|
auto color = dark ? QColor(255, 255, 255) : QColor(0, 0, 0);
|
|
|
|
|
color.setAlpha(collapsed ? (dark ? 32 : 30) : (dark ? 26 : 24));
|
|
|
|
|
return color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QColor selected_tab_shadow(QPalette const& palette, int layer)
|
|
|
|
|
{
|
|
|
|
|
auto dark = ChromeStyle::is_dark(palette);
|
|
|
|
|
auto color = QColor(0, 0, 0);
|
|
|
|
|
if (layer == 0)
|
|
|
|
|
color.setAlpha(dark ? 112 : 22);
|
|
|
|
|
else
|
|
|
|
|
color.setAlpha(dark ? 50 : 10);
|
|
|
|
|
return color;
|
2026-06-01 06:09:45 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 11:21:08 -03:00
|
|
|
static QRectF collapsed_vertical_tab_shape_rect(QRectF const& rect)
|
|
|
|
|
{
|
|
|
|
|
return rect.adjusted(4.0, 3.0, -4.0, -3.0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QRect collapsed_vertical_tab_shape_rect(QRect const& rect)
|
|
|
|
|
{
|
|
|
|
|
return rect.adjusted(4, 3, -4, -3);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
class NewTabButton final : public QToolButton {
|
|
|
|
|
public:
|
2026-06-02 16:24:13 -03:00
|
|
|
explicit NewTabButton(TabBar& tab_bar, QWidget* parent)
|
2026-06-01 05:06:15 -03:00
|
|
|
: QToolButton(parent)
|
|
|
|
|
, m_tab_bar(tab_bar)
|
|
|
|
|
{
|
|
|
|
|
}
|
2026-05-31 19:37:00 -03:00
|
|
|
|
|
|
|
|
private:
|
2026-06-02 16:19:00 -03:00
|
|
|
bool is_hovered() const
|
|
|
|
|
{
|
|
|
|
|
return rect().contains(mapFromGlobal(QCursor::pos()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
virtual void enterEvent(QEnterEvent* event) override
|
|
|
|
|
{
|
|
|
|
|
QToolButton::enterEvent(event);
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
virtual void leaveEvent(QEvent* event) override
|
|
|
|
|
{
|
|
|
|
|
QToolButton::leaveEvent(event);
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
virtual void paintEvent(QPaintEvent* event) override
|
|
|
|
|
{
|
2026-06-02 12:45:44 -03:00
|
|
|
if (!property(VERTICAL_TABS_BUTTON_PROPERTY).toBool()) {
|
2026-05-31 19:37:00 -03:00
|
|
|
QToolButton::paintEvent(event);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
auto expanded = property(VERTICAL_TABS_EXPANDED_PROPERTY).toBool();
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
QPainter painter(this);
|
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
|
|
|
|
|
|
auto dark = ChromeStyle::is_dark(palette());
|
2026-06-02 16:24:13 -03:00
|
|
|
auto is_horizontal_new_tab_button = m_tab_bar.tab_layout() == TabLayout::Horizontal;
|
2026-06-02 12:45:44 -03:00
|
|
|
auto shape_rect = expanded
|
|
|
|
|
? tab_card_shape_rect(QRectF(rect()))
|
2026-06-02 16:24:13 -03:00
|
|
|
: (is_horizontal_new_tab_button
|
|
|
|
|
? horizontal_new_tab_button_shape_rect(QRectF(rect()))
|
|
|
|
|
: QRectF(rect()).adjusted(4.0, 3.0, -4.0, -3.0));
|
2026-05-31 19:37:00 -03:00
|
|
|
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
|
2026-06-02 16:19:00 -03:00
|
|
|
auto hovered = is_hovered();
|
2026-05-31 19:37:00 -03:00
|
|
|
|
|
|
|
|
if (isDown()) {
|
|
|
|
|
painter.setBrush(ChromeStyle::chrome_surface_pressed(palette()));
|
|
|
|
|
painter.setPen(QPen(ChromeStyle::chrome_control_border(palette()), 1));
|
|
|
|
|
painter.drawPath(tab_path);
|
2026-06-02 16:19:00 -03:00
|
|
|
} else if (hovered) {
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.setBrush(tab_hover_surface(palette(), 1.0));
|
|
|
|
|
painter.setPen(Qt::NoPen);
|
2026-05-31 19:37:00 -03:00
|
|
|
painter.drawPath(tab_path);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 16:17:36 -03:00
|
|
|
auto contents_rect = shape_rect.toAlignedRect().adjusted(TAB_CONTENT_HORIZONTAL_INSET, 0, -TAB_CONTENT_HORIZONTAL_INSET, 0);
|
2026-06-02 12:45:44 -03:00
|
|
|
auto icon_size = QSize(TAB_ICON_SIZE, TAB_ICON_SIZE);
|
2026-05-31 19:37:00 -03:00
|
|
|
QRect icon_rect {
|
2026-06-02 12:45:44 -03:00
|
|
|
expanded ? contents_rect.left() - ((icon_size.width() - TAB_ICON_SIZE) / 2) : rect().center().x() - (icon_size.width() / 2),
|
2026-06-01 11:24:31 -03:00
|
|
|
contents_rect.top() + ((contents_rect.height() - icon_size.height()) / 2),
|
|
|
|
|
icon_size.width(),
|
|
|
|
|
icon_size.height(),
|
2026-05-31 19:37:00 -03:00
|
|
|
};
|
2026-06-02 16:24:13 -03:00
|
|
|
if (is_horizontal_new_tab_button) {
|
|
|
|
|
icon_rect.translate(0, 1);
|
|
|
|
|
painter.translate(0.5, 0);
|
|
|
|
|
} else if (!expanded) {
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.translate(1.0, 0);
|
2026-06-02 16:24:13 -03:00
|
|
|
}
|
2026-05-31 19:37:00 -03:00
|
|
|
icon().paint(&painter, icon_rect);
|
2026-06-02 12:45:44 -03:00
|
|
|
if (!expanded)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
contents_rect.setLeft(icon_rect.right() + 8);
|
|
|
|
|
|
|
|
|
|
auto text_color = ChromeStyle::mix(ChromeStyle::chrome_button_text(palette()), ChromeStyle::chrome_muted_text(palette()), dark ? 0.26 : 0.40);
|
2026-06-02 16:19:00 -03:00
|
|
|
if (hovered || isDown())
|
2026-05-31 19:37:00 -03:00
|
|
|
text_color = ChromeStyle::chrome_button_text(palette());
|
|
|
|
|
painter.setPen(text_color);
|
2026-06-01 05:06:15 -03:00
|
|
|
auto text_font = m_tab_bar.font();
|
|
|
|
|
painter.setFont(text_font);
|
2026-05-31 19:37:00 -03:00
|
|
|
|
2026-06-01 05:06:15 -03:00
|
|
|
QFontMetrics font_metrics(text_font);
|
2026-05-31 19:37:00 -03:00
|
|
|
auto title = font_metrics.elidedText(text(), Qt::ElideRight, max(0, contents_rect.width()));
|
|
|
|
|
contents_rect.translate(0, -1);
|
|
|
|
|
painter.drawText(contents_rect, Qt::AlignLeft | Qt::AlignVCenter, title);
|
|
|
|
|
}
|
2026-06-01 05:06:15 -03:00
|
|
|
|
2026-06-02 16:24:13 -03:00
|
|
|
TabBar& m_tab_bar;
|
2026-05-31 19:37:00 -03:00
|
|
|
};
|
|
|
|
|
|
2026-06-10 21:26:41 -03:00
|
|
|
class TabPreviewThumbnail final : public QWidget {
|
|
|
|
|
public:
|
|
|
|
|
explicit TabPreviewThumbnail(QWidget* parent)
|
|
|
|
|
: QWidget(parent)
|
|
|
|
|
{
|
|
|
|
|
setFixedSize(TAB_PREVIEW_THUMBNAIL_WIDTH, TAB_PREVIEW_THUMBNAIL_HEIGHT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void set_colors(QColor background, QColor border)
|
|
|
|
|
{
|
|
|
|
|
m_background = background;
|
|
|
|
|
m_border = border;
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void set_pixmap(QPixmap pixmap)
|
|
|
|
|
{
|
|
|
|
|
m_pixmap = AK::move(pixmap);
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
virtual void paintEvent(QPaintEvent*) override
|
|
|
|
|
{
|
|
|
|
|
QPainter painter(this);
|
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
|
|
|
|
|
auto outer_rect = rect();
|
|
|
|
|
outer_rect.adjust(0, 0, -1, -1);
|
|
|
|
|
|
|
|
|
|
QPainterPath outer_path;
|
|
|
|
|
outer_path.addRoundedRect(outer_rect, 7, 7);
|
|
|
|
|
painter.fillPath(outer_path, m_background);
|
|
|
|
|
|
|
|
|
|
if (!m_pixmap.isNull()) {
|
|
|
|
|
auto image_rect = rect().adjusted(1, 1, -1, -1);
|
|
|
|
|
QPainterPath image_path;
|
|
|
|
|
image_path.addRoundedRect(image_rect, 6, 6);
|
|
|
|
|
|
|
|
|
|
painter.save();
|
|
|
|
|
painter.setClipPath(image_path);
|
|
|
|
|
|
|
|
|
|
auto x = image_rect.x() + (image_rect.width() - m_pixmap.width()) / 2;
|
|
|
|
|
auto y = image_rect.y() + (image_rect.height() - m_pixmap.height()) / 2;
|
|
|
|
|
painter.drawPixmap(x, y, m_pixmap);
|
|
|
|
|
painter.restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
painter.setPen(m_border);
|
|
|
|
|
painter.drawPath(outer_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QColor m_background;
|
|
|
|
|
QColor m_border;
|
|
|
|
|
QPixmap m_pixmap;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class TabPreviewPopup final : public QWidget {
|
|
|
|
|
public:
|
|
|
|
|
explicit TabPreviewPopup(QWidget* parent)
|
|
|
|
|
: QWidget(parent, Qt::ToolTip | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint)
|
|
|
|
|
{
|
|
|
|
|
setAttribute(Qt::WA_TranslucentBackground);
|
|
|
|
|
setAttribute(Qt::WA_ShowWithoutActivating);
|
|
|
|
|
|
|
|
|
|
auto* outer_layout = new QVBoxLayout(this);
|
|
|
|
|
outer_layout->setContentsMargins(TAB_PREVIEW_SHADOW_MARGIN, TAB_PREVIEW_SHADOW_MARGIN, TAB_PREVIEW_SHADOW_MARGIN, TAB_PREVIEW_SHADOW_MARGIN);
|
|
|
|
|
outer_layout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
m_card = new QFrame(this);
|
|
|
|
|
m_card->setObjectName("LadybirdTabPreviewCard");
|
|
|
|
|
|
|
|
|
|
auto* shadow = new QGraphicsDropShadowEffect(m_card);
|
|
|
|
|
shadow->setBlurRadius(28);
|
|
|
|
|
shadow->setOffset(0, 8);
|
|
|
|
|
shadow->setColor(QColor(0, 0, 0, 92));
|
|
|
|
|
m_card->setGraphicsEffect(shadow);
|
|
|
|
|
|
|
|
|
|
auto* card_layout = new QVBoxLayout(m_card);
|
|
|
|
|
card_layout->setContentsMargins(10, 10, 10, 10);
|
|
|
|
|
card_layout->setSpacing(8);
|
|
|
|
|
|
|
|
|
|
m_thumbnail = new TabPreviewThumbnail(m_card);
|
|
|
|
|
|
|
|
|
|
m_title_label = new QLabel(m_card);
|
|
|
|
|
m_title_label->setObjectName("LadybirdTabPreviewTitle");
|
|
|
|
|
m_title_label->setFixedWidth(TAB_PREVIEW_THUMBNAIL_WIDTH);
|
|
|
|
|
m_title_label->setTextFormat(Qt::PlainText);
|
|
|
|
|
|
|
|
|
|
m_url_label = new QLabel(m_card);
|
|
|
|
|
m_url_label->setObjectName("LadybirdTabPreviewURL");
|
|
|
|
|
m_url_label->setFixedWidth(TAB_PREVIEW_THUMBNAIL_WIDTH);
|
|
|
|
|
m_url_label->setTextFormat(Qt::PlainText);
|
|
|
|
|
|
|
|
|
|
card_layout->addWidget(m_thumbnail);
|
|
|
|
|
card_layout->addWidget(m_title_label);
|
|
|
|
|
card_layout->addWidget(m_url_label);
|
|
|
|
|
outer_layout->addWidget(m_card);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void set_preview(QPalette const& palette, QString title, QString url, QPixmap const& thumbnail)
|
|
|
|
|
{
|
|
|
|
|
update_chrome_style(palette);
|
|
|
|
|
|
|
|
|
|
if (title.trimmed().isEmpty())
|
|
|
|
|
title = "Untitled";
|
|
|
|
|
|
|
|
|
|
QFontMetrics title_metrics(m_title_label->font());
|
|
|
|
|
QFontMetrics url_metrics(m_url_label->font());
|
|
|
|
|
|
|
|
|
|
m_thumbnail->set_pixmap(thumbnail);
|
|
|
|
|
m_title_label->setText(title_metrics.elidedText(title, Qt::ElideRight, TAB_PREVIEW_THUMBNAIL_WIDTH));
|
|
|
|
|
m_url_label->setText(url_metrics.elidedText(url, Qt::ElideMiddle, TAB_PREVIEW_THUMBNAIL_WIDTH));
|
|
|
|
|
adjustSize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
void update_chrome_style(QPalette const& palette)
|
|
|
|
|
{
|
|
|
|
|
auto card_background = ChromeStyle::style_sheet_color(ChromeStyle::chrome_surface(palette));
|
|
|
|
|
auto thumbnail_background = ChromeStyle::chrome_surface_recessed(palette);
|
|
|
|
|
auto border_color = ChromeStyle::chrome_control_border(palette);
|
|
|
|
|
auto border = ChromeStyle::style_sheet_color(border_color);
|
|
|
|
|
auto text = ChromeStyle::style_sheet_color(ChromeStyle::chrome_text(palette));
|
|
|
|
|
auto muted_text = ChromeStyle::style_sheet_color(ChromeStyle::chrome_muted_text(palette));
|
|
|
|
|
|
|
|
|
|
m_thumbnail->set_colors(thumbnail_background, border_color);
|
|
|
|
|
|
|
|
|
|
setStyleSheet(qformatted(R"(
|
|
|
|
|
QFrame#LadybirdTabPreviewCard {{
|
|
|
|
|
background: {};
|
|
|
|
|
border: 1px solid {};
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
QLabel#LadybirdTabPreviewTitle {{
|
|
|
|
|
color: {};
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
QLabel#LadybirdTabPreviewURL {{
|
|
|
|
|
color: {};
|
|
|
|
|
}}
|
|
|
|
|
)",
|
|
|
|
|
card_background, border, text, muted_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QFrame* m_card { nullptr };
|
|
|
|
|
TabPreviewThumbnail* m_thumbnail { nullptr };
|
|
|
|
|
QLabel* m_title_label { nullptr };
|
|
|
|
|
QLabel* m_url_label { nullptr };
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-22 17:56:51 -03:00
|
|
|
TabBar::TabBar(TabWidget* tab_widget)
|
|
|
|
|
: QTabBar(tab_widget)
|
|
|
|
|
, m_tab_widget(tab_widget)
|
2024-04-28 11:07:23 -03:00
|
|
|
{
|
2026-05-25 16:21:50 -03:00
|
|
|
setMouseTracking(true);
|
2026-05-25 16:22:07 -03:00
|
|
|
setAcceptDrops(true);
|
2026-05-25 16:22:32 -03:00
|
|
|
setFocusPolicy(Qt::NoFocus);
|
|
|
|
|
setIconSize({ 16, 16 });
|
2026-05-31 12:58:57 -03:00
|
|
|
setMinimumHeight(39);
|
2026-05-31 21:48:32 -03:00
|
|
|
recreate_icons();
|
2026-05-25 16:22:32 -03:00
|
|
|
|
2026-06-10 21:26:41 -03:00
|
|
|
m_tab_preview_timer = new QTimer(this);
|
|
|
|
|
m_tab_preview_timer->setInterval(TAB_PREVIEW_HOVER_DELAY_MS);
|
|
|
|
|
m_tab_preview_timer->setSingleShot(true);
|
|
|
|
|
connect(m_tab_preview_timer, &QTimer::timeout, this, &TabBar::show_tab_preview);
|
|
|
|
|
|
|
|
|
|
m_tab_preview_popup = new TabPreviewPopup(this);
|
|
|
|
|
|
2026-05-25 16:22:32 -03:00
|
|
|
m_hover_animation = new QVariantAnimation(this);
|
|
|
|
|
m_hover_animation->setDuration(120);
|
|
|
|
|
m_hover_animation->setEasingCurve(QEasingCurve::OutCubic);
|
|
|
|
|
connect(m_hover_animation, &QVariantAnimation::valueChanged, this, [this](QVariant const& value) {
|
|
|
|
|
m_hover_progress = value.toReal();
|
|
|
|
|
update();
|
|
|
|
|
});
|
|
|
|
|
connect(m_hover_animation, &QVariantAnimation::finished, this, [this] {
|
|
|
|
|
if (m_hover_progress <= 0.0)
|
|
|
|
|
m_hover_animation_tab_index = -1;
|
|
|
|
|
});
|
2026-05-31 14:17:47 -03:00
|
|
|
connect(this, &QTabBar::currentChanged, this, [this](int index) {
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-05-31 14:17:47 -03:00
|
|
|
ensure_tab_visible(index);
|
2026-06-01 05:50:46 -03:00
|
|
|
update_tab_button_geometry();
|
2026-05-31 14:17:47 -03:00
|
|
|
});
|
2024-04-28 11:07:23 -03:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
void TabBar::set_available_width(int width)
|
|
|
|
|
{
|
|
|
|
|
if (m_available_width != width) {
|
|
|
|
|
m_available_width = width;
|
2026-05-31 14:17:47 -03:00
|
|
|
refresh_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::set_tab_layout(TabLayout tab_layout)
|
|
|
|
|
{
|
|
|
|
|
if (m_tab_layout == tab_layout)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_tab_layout = tab_layout;
|
|
|
|
|
|
|
|
|
|
if (m_tab_layout == TabLayout::Horizontal) {
|
|
|
|
|
setShape(QTabBar::RoundedNorth);
|
2026-06-02 12:45:44 -03:00
|
|
|
setMinimumSize({ 0, HORIZONTAL_TAB_HEIGHT });
|
2026-05-31 14:17:47 -03:00
|
|
|
setMaximumWidth(QWIDGETSIZE_MAX);
|
|
|
|
|
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
|
|
|
|
setUsesScrollButtons(true);
|
|
|
|
|
set_vertical_scroll_offset(0);
|
|
|
|
|
} else {
|
|
|
|
|
setShape(QTabBar::RoundedWest);
|
|
|
|
|
setMinimumSize({ 0, 0 });
|
|
|
|
|
setMaximumWidth(QWIDGETSIZE_MAX);
|
|
|
|
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
|
|
|
setUsesScrollButtons(false);
|
2026-03-17 08:38:25 -03:00
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
refresh_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::refresh_tab_layout()
|
|
|
|
|
{
|
|
|
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset);
|
|
|
|
|
updateGeometry();
|
|
|
|
|
update_tab_button_geometry();
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 21:48:32 -03:00
|
|
|
void TabBar::recreate_icons()
|
|
|
|
|
{
|
|
|
|
|
m_fallback_tab_icon = create_chrome_icon(ChromeIcon::Globe, palette());
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
QSize TabBar::sizeHint() const
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() != TabLayout::Horizontal)
|
|
|
|
|
return vertical_size_hint(count());
|
|
|
|
|
return QTabBar::sizeHint();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QSize TabBar::minimumSizeHint() const
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() != TabLayout::Horizontal)
|
|
|
|
|
return vertical_size_hint(count() > 0 ? 1 : 0);
|
|
|
|
|
return QTabBar::minimumSizeHint();
|
2026-03-17 08:38:25 -03:00
|
|
|
}
|
|
|
|
|
|
2024-04-02 22:38:01 -03:00
|
|
|
QSize TabBar::tabSizeHint(int index) const
|
|
|
|
|
{
|
|
|
|
|
auto hint = QTabBar::tabSizeHint(index);
|
2024-08-30 13:24:21 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
if (tab_layout() != TabLayout::Horizontal) {
|
|
|
|
|
hint.setWidth(vertical_tab_width(m_available_width, tab_layout()));
|
|
|
|
|
hint.setHeight(VERTICAL_TAB_HEIGHT);
|
|
|
|
|
return hint;
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-30 13:24:21 -03:00
|
|
|
if (auto count = this->count(); count > 0) {
|
2026-03-17 08:38:25 -03:00
|
|
|
auto width = (m_available_width > 0 ? m_available_width : this->width()) / count;
|
2026-06-01 08:41:54 -03:00
|
|
|
width = min(HORIZONTAL_TAB_MAX_WIDTH, width);
|
|
|
|
|
width = max(HORIZONTAL_TAB_MIN_WIDTH, width);
|
2024-08-30 13:24:21 -03:00
|
|
|
|
|
|
|
|
hint.setWidth(width);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
hint.setHeight(HORIZONTAL_TAB_HEIGHT);
|
2024-04-02 22:38:01 -03:00
|
|
|
return hint;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabBar::resizeEvent(QResizeEvent* event)
|
2026-05-25 16:21:50 -03:00
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-05-31 14:17:47 -03:00
|
|
|
QTabBar::resizeEvent(event);
|
|
|
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset);
|
|
|
|
|
ensure_tab_visible(currentIndex());
|
|
|
|
|
update_tab_button_geometry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::tabLayoutChange()
|
|
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-05-31 14:17:47 -03:00
|
|
|
QTabBar::tabLayoutChange();
|
|
|
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset);
|
|
|
|
|
update_tab_button_geometry();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 21:26:41 -03:00
|
|
|
bool TabBar::event(QEvent* event)
|
|
|
|
|
{
|
|
|
|
|
if (event->type() == QEvent::ToolTip) {
|
|
|
|
|
auto* help_event = static_cast<QHelpEvent*>(event);
|
|
|
|
|
auto hovered_tab = tab_index_at(help_event->pos());
|
|
|
|
|
if (hovered_tab >= 0 && hovered_tab != currentIndex()) {
|
|
|
|
|
if (auto* tab = m_tab_widget ? m_tab_widget->tab(hovered_tab) : nullptr) {
|
|
|
|
|
if (tab->view().tab_preview_pixmap({ TAB_PREVIEW_THUMBNAIL_WIDTH, TAB_PREVIEW_THUMBNAIL_HEIGHT }).has_value()) {
|
|
|
|
|
event->accept();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return QTabBar::event(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabBar::paintEvent(QPaintEvent*)
|
|
|
|
|
{
|
2026-05-25 16:21:50 -03:00
|
|
|
QPainter painter(this);
|
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
|
|
|
|
|
|
auto border = ChromeStyle::chrome_border(palette());
|
2026-05-25 16:22:32 -03:00
|
|
|
auto dark = ChromeStyle::is_dark(palette());
|
2026-05-31 12:59:53 -03:00
|
|
|
auto text_color = ChromeStyle::chrome_text(palette());
|
2026-05-31 14:17:47 -03:00
|
|
|
auto is_vertical = tab_layout() != TabLayout::Horizontal;
|
|
|
|
|
auto is_collapsed_vertical = tab_layout() == TabLayout::VerticalCollapsed;
|
2026-05-25 16:21:50 -03:00
|
|
|
|
|
|
|
|
for (int index = 0; index < count(); ++index) {
|
2026-05-31 14:17:47 -03:00
|
|
|
auto tab_rect = visual_tab_rect(index);
|
|
|
|
|
if (!tab_rect.isValid())
|
|
|
|
|
continue;
|
|
|
|
|
if (is_vertical && (tab_rect.bottom() < 0 || tab_rect.top() > height()))
|
|
|
|
|
continue;
|
|
|
|
|
if (!is_vertical && (tab_rect.right() < 0 || tab_rect.left() > width()))
|
2026-05-25 16:21:50 -03:00
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
bool is_selected = index == currentIndex();
|
2026-05-25 16:22:32 -03:00
|
|
|
auto hover_progress = index == m_hover_animation_tab_index ? m_hover_progress : (index == m_hovered_tab_index ? 1.0 : 0.0);
|
|
|
|
|
bool is_hovered = hover_progress > 0.0;
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
QRectF shape_rect;
|
|
|
|
|
if (is_collapsed_vertical)
|
2026-06-02 11:21:08 -03:00
|
|
|
shape_rect = collapsed_vertical_tab_shape_rect(QRectF(tab_rect));
|
2026-06-02 12:45:44 -03:00
|
|
|
else if (m_tab_layout == TabLayout::Horizontal)
|
|
|
|
|
shape_rect = horizontal_tab_card_shape_rect(QRectF(tab_rect));
|
2026-05-31 14:17:47 -03:00
|
|
|
else
|
2026-06-02 12:45:44 -03:00
|
|
|
shape_rect = tab_card_shape_rect(QRectF(tab_rect));
|
|
|
|
|
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
|
2026-05-25 16:22:32 -03:00
|
|
|
|
|
|
|
|
if (is_selected) {
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.setPen(Qt::NoPen);
|
|
|
|
|
painter.setBrush(selected_tab_shadow(palette(), 1));
|
|
|
|
|
painter.drawPath(tab_path.translated(0, 2));
|
|
|
|
|
painter.setBrush(selected_tab_shadow(palette(), 0));
|
|
|
|
|
painter.drawPath(tab_path.translated(0, 1));
|
|
|
|
|
|
2026-05-25 16:22:32 -03:00
|
|
|
auto selected_gradient = QLinearGradient(shape_rect.topLeft(), shape_rect.bottomLeft());
|
2026-05-26 09:17:10 -03:00
|
|
|
selected_gradient.setColorAt(0.0, ChromeStyle::chrome_active_tab_surface_top(palette()));
|
|
|
|
|
selected_gradient.setColorAt(1.0, ChromeStyle::chrome_active_tab_surface_bottom(palette()));
|
2026-05-25 16:22:32 -03:00
|
|
|
painter.setBrush(selected_gradient);
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.setPen(QPen(selected_tab_border(palette(), is_collapsed_vertical), 1));
|
2026-05-25 16:21:50 -03:00
|
|
|
painter.drawPath(tab_path);
|
2026-05-25 16:22:32 -03:00
|
|
|
} else if (is_hovered) {
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.setBrush(tab_hover_surface(palette(), hover_progress));
|
|
|
|
|
painter.setPen(Qt::NoPen);
|
2026-05-25 16:22:32 -03:00
|
|
|
painter.drawPath(tab_path);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
if (!is_selected && !is_hovered && index > 0 && index != currentIndex() + 1 && !is_collapsed_vertical) {
|
2026-05-25 16:21:50 -03:00
|
|
|
auto separator = border;
|
2026-06-02 12:45:44 -03:00
|
|
|
separator.setAlpha(dark ? 24 : 20);
|
2026-05-25 16:21:50 -03:00
|
|
|
painter.setPen(separator);
|
2026-05-31 14:17:47 -03:00
|
|
|
if (is_vertical)
|
|
|
|
|
painter.drawLine(QPoint(tab_rect.left() + 16, tab_rect.top()), QPoint(tab_rect.right() - 16, tab_rect.top()));
|
|
|
|
|
else
|
|
|
|
|
painter.drawLine(QPoint(tab_rect.left(), 15), QPoint(tab_rect.left(), height() - 15));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto icon = tabIcon(index);
|
2026-05-31 15:39:22 -03:00
|
|
|
if (icon.isNull())
|
2026-05-31 21:48:32 -03:00
|
|
|
icon = m_fallback_tab_icon;
|
2026-05-31 15:39:22 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
if (is_collapsed_vertical) {
|
2026-05-31 15:39:22 -03:00
|
|
|
QRect icon_rect {
|
2026-05-31 19:37:00 -03:00
|
|
|
tab_rect.center().x() - (TAB_ICON_SIZE / 2) + 1,
|
|
|
|
|
tab_rect.center().y() - (TAB_ICON_SIZE / 2) + 1,
|
2026-05-31 15:39:22 -03:00
|
|
|
TAB_ICON_SIZE,
|
|
|
|
|
TAB_ICON_SIZE,
|
|
|
|
|
};
|
2026-06-02 12:45:44 -03:00
|
|
|
if (!is_selected)
|
|
|
|
|
painter.setOpacity(is_hovered ? 0.92 : (dark ? 0.88 : 0.84));
|
2026-05-31 15:39:22 -03:00
|
|
|
icon.paint(&painter, icon_rect);
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.setOpacity(1.0);
|
2026-05-31 14:17:47 -03:00
|
|
|
continue;
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 16:17:36 -03:00
|
|
|
auto contents_rect = shape_rect.toAlignedRect().adjusted(TAB_CONTENT_HORIZONTAL_INSET, 0, -TAB_CONTENT_HORIZONTAL_INSET, 0);
|
2026-05-25 16:21:50 -03:00
|
|
|
if (auto* left_button = tabButton(index, QTabBar::LeftSide); left_button && left_button->isVisible())
|
|
|
|
|
contents_rect.setLeft(max(contents_rect.left(), left_button->geometry().right() + 6));
|
|
|
|
|
if (auto* right_button = tabButton(index, QTabBar::RightSide); right_button && right_button->isVisible())
|
|
|
|
|
contents_rect.setRight(min(contents_rect.right(), right_button->geometry().left() - 6));
|
|
|
|
|
|
2026-05-31 15:39:22 -03:00
|
|
|
if (contents_rect.width() > 26) {
|
2026-05-25 16:21:50 -03:00
|
|
|
QRect icon_rect {
|
|
|
|
|
contents_rect.left(),
|
2026-05-31 14:17:47 -03:00
|
|
|
contents_rect.top() + ((contents_rect.height() - TAB_ICON_SIZE) / 2),
|
|
|
|
|
TAB_ICON_SIZE,
|
|
|
|
|
TAB_ICON_SIZE,
|
2026-05-25 16:21:50 -03:00
|
|
|
};
|
2026-06-02 12:45:44 -03:00
|
|
|
if (!is_selected)
|
|
|
|
|
painter.setOpacity(is_hovered ? 0.92 : (dark ? 0.88 : 0.84));
|
2026-05-25 16:21:50 -03:00
|
|
|
icon.paint(&painter, icon_rect);
|
2026-06-02 12:45:44 -03:00
|
|
|
painter.setOpacity(1.0);
|
2026-06-02 16:17:36 -03:00
|
|
|
contents_rect.setLeft(icon_rect.right() + 8);
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QFont tab_font = font();
|
|
|
|
|
if (is_selected)
|
|
|
|
|
tab_font.setWeight(QFont::DemiBold);
|
|
|
|
|
painter.setFont(tab_font);
|
|
|
|
|
|
|
|
|
|
QFontMetrics font_metrics(tab_font);
|
|
|
|
|
auto title = font_metrics.elidedText(tabText(index), Qt::ElideRight, max(0, contents_rect.width()));
|
2026-06-02 12:45:44 -03:00
|
|
|
auto tab_text_color = text_color;
|
|
|
|
|
if (!is_selected)
|
|
|
|
|
tab_text_color.setAlpha(is_hovered ? (dark ? 236 : 228) : (dark ? 226 : 216));
|
|
|
|
|
painter.setPen(tab_text_color);
|
2026-05-25 16:21:50 -03:00
|
|
|
painter.drawText(contents_rect, Qt::AlignLeft | Qt::AlignVCenter, title);
|
|
|
|
|
}
|
2026-05-25 16:22:07 -03:00
|
|
|
|
|
|
|
|
if (m_drop_indicator_index >= 0 && count() > 0) {
|
|
|
|
|
auto indicator_color = ChromeStyle::chrome_accent(palette());
|
|
|
|
|
indicator_color.setAlpha(220);
|
|
|
|
|
painter.setPen(QPen(indicator_color, 3, Qt::SolidLine, Qt::RoundCap));
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
if (is_vertical) {
|
|
|
|
|
auto indicator_y = m_drop_indicator_index >= count()
|
|
|
|
|
? visual_tab_rect(count() - 1).bottom() + 3
|
|
|
|
|
: visual_tab_rect(m_drop_indicator_index).top() + 1;
|
|
|
|
|
indicator_y = max(3, min(height() - 3, indicator_y));
|
|
|
|
|
painter.drawLine(QPointF(10, indicator_y), QPointF(width() - 10, indicator_y));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto indicator_x = m_drop_indicator_index >= count()
|
|
|
|
|
? visual_tab_rect(count() - 1).right() + 3
|
|
|
|
|
: visual_tab_rect(m_drop_indicator_index).left() + 1;
|
|
|
|
|
indicator_x = max(2, min(width() - 3, indicator_x));
|
|
|
|
|
|
2026-05-31 12:58:57 -03:00
|
|
|
painter.drawLine(QPointF(indicator_x, 8), QPointF(indicator_x, height() - 6));
|
2026-05-25 16:22:07 -03:00
|
|
|
}
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
|
|
|
|
|
2024-04-28 11:07:23 -03:00
|
|
|
void TabBar::contextMenuEvent(QContextMenuEvent* event)
|
|
|
|
|
{
|
2026-03-22 17:56:51 -03:00
|
|
|
if (!m_tab_widget)
|
|
|
|
|
return;
|
2026-03-17 08:38:25 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
auto tab_index = tab_index_at(event->pos());
|
2026-05-25 16:21:50 -03:00
|
|
|
if (tab_index < 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (auto* tab = m_tab_widget->tab(tab_index))
|
2024-04-28 11:07:23 -03:00
|
|
|
tab->context_menu()->exec(event->globalPos());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
void TabBar::dragEnterEvent(QDragEnterEvent* event)
|
2024-09-17 09:28:17 -03:00
|
|
|
{
|
2026-05-25 16:22:07 -03:00
|
|
|
if (!m_tab_widget || !s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
|
|
|
event->ignore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-09-17 09:28:17 -03:00
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::dragLeaveEvent(QDragLeaveEvent* event)
|
|
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-05-25 16:22:07 -03:00
|
|
|
m_drop_indicator_index = -1;
|
|
|
|
|
update();
|
|
|
|
|
QTabBar::dragLeaveEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::dragMoveEvent(QDragMoveEvent* event)
|
|
|
|
|
{
|
|
|
|
|
if (!m_tab_widget || !s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
|
|
|
event->ignore();
|
|
|
|
|
return;
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
auto drop_indicator_index = drop_indicator_index_for_insertion_index(insertion_index_at(event->position().toPoint()));
|
|
|
|
|
if (m_drop_indicator_index != drop_indicator_index) {
|
|
|
|
|
m_drop_indicator_index = drop_indicator_index;
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::dropEvent(QDropEvent* event)
|
|
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-05-25 16:22:07 -03:00
|
|
|
if (!m_tab_widget || !s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
|
|
|
event->ignore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_pending_tab_drop_target = m_tab_widget;
|
|
|
|
|
s_pending_tab_drop_index = insertion_index_at(event->position().toPoint());
|
|
|
|
|
m_drop_indicator_index = -1;
|
|
|
|
|
update();
|
|
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 11:21:08 -03:00
|
|
|
bool TabBar::eventFilter(QObject* watched, QEvent* event)
|
|
|
|
|
{
|
|
|
|
|
auto hovered_tab_for_button = [this](QObject* button) {
|
|
|
|
|
for (int index = 0; index < count(); ++index) {
|
|
|
|
|
if (tabButton(index, QTabBar::LeftSide) == button || tabButton(index, QTabBar::RightSide) == button)
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (auto hovered_tab = hovered_tab_for_button(watched); hovered_tab >= 0) {
|
|
|
|
|
if (event->type() == QEvent::Enter || event->type() == QEvent::MouseMove) {
|
|
|
|
|
set_hovered_tab_index(hovered_tab);
|
|
|
|
|
} else if (event->type() == QEvent::Leave) {
|
|
|
|
|
auto cursor_position = mapFromGlobal(QCursor::pos());
|
|
|
|
|
set_hovered_tab_index(tab_index_at(cursor_position));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return QTabBar::eventFilter(watched, event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
void TabBar::mousePressEvent(QMouseEvent* event)
|
|
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-05-31 14:17:47 -03:00
|
|
|
auto pressed_tab = tab_index_at(event->pos());
|
2026-05-25 16:22:07 -03:00
|
|
|
m_pressed_tab = (m_tab_widget && pressed_tab >= 0) ? m_tab_widget->tab(pressed_tab) : nullptr;
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
if (pressed_tab >= 0) {
|
2026-05-31 14:17:47 -03:00
|
|
|
auto rect_of_current_tab = visual_tab_rect(pressed_tab);
|
2026-05-25 16:22:07 -03:00
|
|
|
m_position_in_selected_tab_while_dragging = event->pos() - rect_of_current_tab.topLeft();
|
|
|
|
|
m_drag_start_position = event->pos();
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
2024-09-17 09:28:17 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
if (tab_layout() != TabLayout::Horizontal) {
|
2026-05-31 19:37:00 -03:00
|
|
|
if (pressed_tab >= 0) {
|
2026-05-31 14:17:47 -03:00
|
|
|
setCurrentIndex(pressed_tab);
|
2026-05-31 19:37:00 -03:00
|
|
|
} else if (event->button() == Qt::LeftButton && start_window_move()) {
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
} else {
|
2026-06-02 12:45:44 -03:00
|
|
|
if (pressed_tab < 0 && event->button() == Qt::LeftButton && start_window_move()) {
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
QTabBar::mousePressEvent(event);
|
|
|
|
|
}
|
2026-05-25 16:22:07 -03:00
|
|
|
|
|
|
|
|
if (m_pressed_tab)
|
|
|
|
|
event->accept();
|
|
|
|
|
else
|
|
|
|
|
event->ignore();
|
2024-09-17 09:28:17 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::mouseMoveEvent(QMouseEvent* event)
|
|
|
|
|
{
|
2026-06-02 15:22:48 -03:00
|
|
|
set_hovered_tab_index(tab_index_at(event->pos()));
|
2026-05-25 16:21:50 -03:00
|
|
|
|
|
|
|
|
if (count() == 0) {
|
2026-05-31 14:17:47 -03:00
|
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
|
|
|
QTabBar::mouseMoveEvent(event);
|
2026-05-25 16:21:50 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
if (m_pressed_tab && event->buttons().testFlag(Qt::LeftButton)) {
|
|
|
|
|
auto pressed_tab_index = m_tab_widget ? m_tab_widget->index_of(m_pressed_tab) : -1;
|
|
|
|
|
if (pressed_tab_index < 0) {
|
|
|
|
|
m_pressed_tab = nullptr;
|
|
|
|
|
} else if ((event->pos() - m_drag_start_position).manhattanLength() >= QApplication::startDragDistance()) {
|
|
|
|
|
start_tab_drag(pressed_tab_index);
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-09-17 09:28:17 -03:00
|
|
|
}
|
2026-05-25 16:22:07 -03:00
|
|
|
|
|
|
|
|
if (m_pressed_tab)
|
|
|
|
|
event->accept();
|
|
|
|
|
else
|
|
|
|
|
event->ignore();
|
2024-09-17 09:28:17 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
void TabBar::leaveEvent(QEvent* event)
|
|
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
2026-06-01 05:50:46 -03:00
|
|
|
set_hovered_tab_index(-1);
|
2026-05-25 16:21:50 -03:00
|
|
|
QTabBar::leaveEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
void TabBar::mouseReleaseEvent(QMouseEvent* event)
|
|
|
|
|
{
|
2026-05-29 08:34:52 -03:00
|
|
|
auto pressed_tab = m_pressed_tab;
|
2026-05-25 16:22:07 -03:00
|
|
|
m_pressed_tab = nullptr;
|
2026-05-29 08:34:52 -03:00
|
|
|
|
|
|
|
|
if (event->button() == Qt::MiddleButton && pressed_tab && m_tab_widget) {
|
2026-05-31 14:17:47 -03:00
|
|
|
if (auto index = m_tab_widget->index_of(pressed_tab); index >= 0 && tab_index_at(event->pos()) == index) {
|
2026-05-29 08:34:52 -03:00
|
|
|
emit tabCloseRequested(index);
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
|
|
|
QTabBar::mouseReleaseEvent(event);
|
2026-05-29 08:34:52 -03:00
|
|
|
|
|
|
|
|
if (pressed_tab)
|
2026-05-25 16:22:07 -03:00
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
void TabBar::mouseDoubleClickEvent(QMouseEvent* event)
|
|
|
|
|
{
|
2026-05-29 10:57:15 -03:00
|
|
|
if (window_uses_client_side_decorations(*this) && tab_index_at(event->pos()) < 0 && event->button() == Qt::LeftButton) {
|
2026-05-25 16:21:50 -03:00
|
|
|
toggle_window_maximized();
|
|
|
|
|
event->accept();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QTabBar::mouseDoubleClickEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabBar::wheelEvent(QWheelEvent* event)
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal || max_vertical_scroll_offset() <= 0) {
|
|
|
|
|
QTabBar::wheelEvent(event);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto old_offset = m_vertical_scroll_offset;
|
|
|
|
|
auto pixel_delta = event->pixelDelta().y();
|
|
|
|
|
auto angle_delta = event->angleDelta().y();
|
|
|
|
|
|
|
|
|
|
if (pixel_delta != 0) {
|
|
|
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset - pixel_delta);
|
|
|
|
|
} else if (angle_delta != 0) {
|
|
|
|
|
auto scroll_delta = angle_delta * VERTICAL_TAB_HEIGHT / 120;
|
|
|
|
|
if (scroll_delta == 0)
|
|
|
|
|
scroll_delta = angle_delta > 0 ? 1 : -1;
|
|
|
|
|
set_vertical_scroll_offset(m_vertical_scroll_offset - scroll_delta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_vertical_scroll_offset != old_offset)
|
|
|
|
|
event->accept();
|
|
|
|
|
else
|
|
|
|
|
event->ignore();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
int TabBar::insertion_index_at(QPoint const& position) const
|
2026-05-25 16:21:50 -03:00
|
|
|
{
|
2026-05-25 16:22:07 -03:00
|
|
|
if (count() == 0)
|
|
|
|
|
return 0;
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
if (tab_layout() != TabLayout::Horizontal) {
|
|
|
|
|
auto unscrolled_y = position.y() + m_vertical_scroll_offset;
|
|
|
|
|
if (unscrolled_y <= 0)
|
|
|
|
|
return 0;
|
|
|
|
|
if (unscrolled_y >= count() * VERTICAL_TAB_HEIGHT)
|
|
|
|
|
return count();
|
|
|
|
|
|
|
|
|
|
auto index = unscrolled_y / VERTICAL_TAB_HEIGHT;
|
|
|
|
|
if (unscrolled_y > (index * VERTICAL_TAB_HEIGHT) + (VERTICAL_TAB_HEIGHT / 2))
|
|
|
|
|
return index + 1;
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto index = tab_index_at(position);
|
2026-05-25 16:22:07 -03:00
|
|
|
if (index < 0)
|
2026-05-31 14:17:47 -03:00
|
|
|
return position.x() < visual_tab_rect(0).center().x() ? 0 : count();
|
2026-05-25 16:22:07 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
if (position.x() > visual_tab_rect(index).center().x())
|
2026-05-25 16:22:07 -03:00
|
|
|
return index + 1;
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int TabBar::drop_indicator_index_for_insertion_index(int insertion_index) const
|
|
|
|
|
{
|
|
|
|
|
if (s_active_tab_drag_source == m_tab_widget && s_active_tab_dragged_tab) {
|
|
|
|
|
auto dragged_tab_index = m_tab_widget->index_of(s_active_tab_dragged_tab);
|
|
|
|
|
if (dragged_tab_index >= 0 && (insertion_index == dragged_tab_index || insertion_index == dragged_tab_index + 1))
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return insertion_index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QPixmap TabBar::render_tab_drag_pixmap(int index) const
|
|
|
|
|
{
|
2026-05-31 14:17:47 -03:00
|
|
|
auto tab_rect = visual_tab_rect(index);
|
2026-05-25 16:22:07 -03:00
|
|
|
QPixmap pixmap(tab_rect.size() * devicePixelRatioF());
|
|
|
|
|
pixmap.setDevicePixelRatio(devicePixelRatioF());
|
|
|
|
|
pixmap.fill(Qt::transparent);
|
|
|
|
|
|
|
|
|
|
QPainter painter(&pixmap);
|
2026-05-29 09:09:41 -03:00
|
|
|
painter.setOpacity(0.75);
|
2026-05-29 08:42:11 -03:00
|
|
|
const_cast<TabBar&>(*this).render(&painter, QPoint(), QRegion(tab_rect), QWidget::DrawChildren);
|
2026-05-25 16:22:07 -03:00
|
|
|
return pixmap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::start_tab_drag(int index)
|
|
|
|
|
{
|
2026-06-10 21:26:41 -03:00
|
|
|
hide_tab_preview();
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
if (!m_tab_widget)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto* tab = m_tab_widget->tab(index);
|
|
|
|
|
if (!tab)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
QPointer<Tab> dragged_tab = tab;
|
|
|
|
|
auto* drag = new QDrag(this);
|
|
|
|
|
auto* mime_data = new QMimeData;
|
|
|
|
|
mime_data->setData(LADYBIRD_TAB_MIME_TYPE, QByteArray {});
|
|
|
|
|
mime_data->setText(tab->title());
|
|
|
|
|
drag->setMimeData(mime_data);
|
|
|
|
|
drag->setPixmap(render_tab_drag_pixmap(index));
|
|
|
|
|
drag->setHotSpot(m_position_in_selected_tab_while_dragging);
|
|
|
|
|
|
|
|
|
|
s_active_tab_drag_source = m_tab_widget;
|
|
|
|
|
s_active_tab_dragged_tab = dragged_tab;
|
|
|
|
|
s_pending_tab_drop_target = nullptr;
|
|
|
|
|
s_pending_tab_drop_index = -1;
|
|
|
|
|
|
|
|
|
|
auto action = drag->exec(Qt::MoveAction, Qt::MoveAction);
|
|
|
|
|
|
|
|
|
|
auto* source_window = qobject_cast<BrowserWindow*>(window());
|
|
|
|
|
if (source_window && dragged_tab) {
|
|
|
|
|
auto current_index = source_window->tab_index(dragged_tab);
|
|
|
|
|
if (current_index >= 0) {
|
|
|
|
|
if (action == Qt::MoveAction && s_pending_tab_drop_target) {
|
|
|
|
|
if (auto* target_window = qobject_cast<BrowserWindow*>(s_pending_tab_drop_target->window()))
|
|
|
|
|
source_window->move_tab_to_window(current_index, *target_window, s_pending_tab_drop_index);
|
2026-05-25 21:12:55 -03:00
|
|
|
} else if (action == Qt::IgnoreAction) {
|
2026-05-31 14:17:47 -03:00
|
|
|
if (!m_tab_widget->tab_strip_global_rect().contains(QCursor::pos()) && m_tab_widget->count() > 1)
|
2026-05-25 21:12:55 -03:00
|
|
|
source_window->detach_tab_to_new_window(current_index, QCursor::pos());
|
2026-05-25 16:22:07 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_active_tab_drag_source = nullptr;
|
|
|
|
|
s_active_tab_dragged_tab = nullptr;
|
|
|
|
|
s_pending_tab_drop_target = nullptr;
|
|
|
|
|
s_pending_tab_drop_index = -1;
|
|
|
|
|
m_pressed_tab = nullptr;
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:32 -03:00
|
|
|
void TabBar::start_hover_animation(int tab_index, qreal target_progress)
|
|
|
|
|
{
|
|
|
|
|
if (!m_hover_animation)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (tab_index < 0) {
|
|
|
|
|
m_hover_animation_tab_index = -1;
|
|
|
|
|
m_hover_progress = 0.0;
|
|
|
|
|
update();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_hover_animation->stop();
|
|
|
|
|
auto start_progress = (m_hover_animation_tab_index == tab_index) ? m_hover_progress : 0.0;
|
|
|
|
|
m_hover_animation_tab_index = tab_index;
|
|
|
|
|
m_hover_animation->setStartValue(start_progress);
|
|
|
|
|
m_hover_animation->setEndValue(target_progress);
|
|
|
|
|
m_hover_animation->start();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 21:26:41 -03:00
|
|
|
void TabBar::schedule_tab_preview(int index)
|
|
|
|
|
{
|
|
|
|
|
if (!m_tab_preview_timer || !m_tab_preview_popup)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (!m_tab_widget || index < 0 || index == currentIndex() || index >= count() || m_pressed_tab) {
|
|
|
|
|
hide_tab_preview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_tab_preview_index == index && m_tab_preview_popup->isVisible())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_tab_preview_index = index;
|
|
|
|
|
m_tab_preview_popup->hide();
|
|
|
|
|
QToolTip::hideText();
|
|
|
|
|
m_tab_preview_timer->start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::show_tab_preview()
|
|
|
|
|
{
|
|
|
|
|
if (!m_tab_widget || !m_tab_preview_popup)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto index = m_tab_preview_index;
|
|
|
|
|
if (index < 0 || index != m_hovered_tab_index || index == currentIndex() || index >= count()) {
|
|
|
|
|
hide_tab_preview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* tab = m_tab_widget->tab(index);
|
|
|
|
|
if (!tab) {
|
|
|
|
|
hide_tab_preview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto thumbnail = tab->view().tab_preview_pixmap({ TAB_PREVIEW_THUMBNAIL_WIDTH, TAB_PREVIEW_THUMBNAIL_HEIGHT });
|
|
|
|
|
if (!thumbnail.has_value()) {
|
|
|
|
|
hide_tab_preview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_tab_preview_popup->set_preview(palette(), tab->title(), qstring_from_ak_string(tab->view().url().serialize()), *thumbnail);
|
|
|
|
|
m_tab_preview_popup->move(tab_preview_position_for(index, m_tab_preview_popup->sizeHint()));
|
|
|
|
|
m_tab_preview_popup->show();
|
|
|
|
|
m_tab_preview_popup->raise();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::hide_tab_preview()
|
|
|
|
|
{
|
|
|
|
|
if (m_tab_preview_timer)
|
|
|
|
|
m_tab_preview_timer->stop();
|
|
|
|
|
|
|
|
|
|
m_tab_preview_index = -1;
|
|
|
|
|
|
|
|
|
|
if (m_tab_preview_popup)
|
|
|
|
|
m_tab_preview_popup->hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QPoint TabBar::tab_preview_position_for(int index, QSize const& popup_size) const
|
|
|
|
|
{
|
|
|
|
|
auto tab_rect = visual_tab_rect(index);
|
|
|
|
|
auto screen = QGuiApplication::screenAt(mapToGlobal(tab_rect.center()));
|
|
|
|
|
if (!screen)
|
|
|
|
|
screen = window()->screen();
|
|
|
|
|
if (!screen)
|
|
|
|
|
screen = QGuiApplication::primaryScreen();
|
|
|
|
|
auto available_geometry = screen ? screen->availableGeometry() : QRect {};
|
|
|
|
|
|
|
|
|
|
QPoint position;
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal) {
|
|
|
|
|
position = mapToGlobal(QPoint {
|
|
|
|
|
tab_rect.center().x() - popup_size.width() / 2,
|
|
|
|
|
tab_rect.bottom() + TAB_PREVIEW_CARD_GAP - TAB_PREVIEW_SHADOW_MARGIN,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (position.y() + popup_size.height() > available_geometry.bottom())
|
|
|
|
|
position.setY(mapToGlobal(QPoint { 0, tab_rect.top() }).y() - popup_size.height() - TAB_PREVIEW_CARD_GAP + TAB_PREVIEW_SHADOW_MARGIN);
|
|
|
|
|
} else {
|
|
|
|
|
position = mapToGlobal(QPoint {
|
|
|
|
|
tab_rect.right() + TAB_PREVIEW_CARD_GAP - TAB_PREVIEW_SHADOW_MARGIN,
|
|
|
|
|
tab_rect.center().y() - popup_size.height() / 2,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (position.x() + popup_size.width() > available_geometry.right())
|
|
|
|
|
position.setX(mapToGlobal(QPoint { tab_rect.left(), 0 }).x() - popup_size.width() - TAB_PREVIEW_CARD_GAP + TAB_PREVIEW_SHADOW_MARGIN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto minimum_x = available_geometry.left() + TAB_PREVIEW_CARD_GAP - TAB_PREVIEW_SHADOW_MARGIN;
|
|
|
|
|
auto maximum_x = available_geometry.right() - popup_size.width() - TAB_PREVIEW_CARD_GAP + TAB_PREVIEW_SHADOW_MARGIN;
|
|
|
|
|
if (minimum_x <= maximum_x)
|
|
|
|
|
position.setX(clamp(position.x(), minimum_x, maximum_x));
|
|
|
|
|
|
|
|
|
|
auto minimum_y = available_geometry.top() + TAB_PREVIEW_CARD_GAP - TAB_PREVIEW_SHADOW_MARGIN;
|
|
|
|
|
auto maximum_y = available_geometry.bottom() - popup_size.height() - TAB_PREVIEW_CARD_GAP + TAB_PREVIEW_SHADOW_MARGIN;
|
|
|
|
|
if (minimum_y <= maximum_y)
|
|
|
|
|
position.setY(clamp(position.y(), minimum_y, maximum_y));
|
|
|
|
|
|
|
|
|
|
return position;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
QRect TabBar::visual_tab_rect(int index) const
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
|
|
|
return tabRect(index);
|
|
|
|
|
|
|
|
|
|
if (index < 0 || index >= count())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto tab_width = width();
|
|
|
|
|
if (tab_width <= 0)
|
|
|
|
|
tab_width = vertical_size_hint(count()).width();
|
|
|
|
|
|
|
|
|
|
return { 0, (index * VERTICAL_TAB_HEIGHT) - m_vertical_scroll_offset, tab_width, VERTICAL_TAB_HEIGHT };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int TabBar::tab_index_at(QPoint const& position) const
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
|
|
|
return tabAt(position);
|
|
|
|
|
|
|
|
|
|
if (position.x() < 0 || position.x() >= width() || position.y() < 0 || position.y() >= height())
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
auto index = (position.y() + m_vertical_scroll_offset) / VERTICAL_TAB_HEIGHT;
|
|
|
|
|
if (index < 0 || index >= count())
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
if (!visual_tab_rect(index).contains(position))
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
return index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QSize TabBar::vertical_size_hint(int tab_count) const
|
|
|
|
|
{
|
|
|
|
|
return { vertical_tab_width(m_available_width, tab_layout()), tab_count * VERTICAL_TAB_HEIGHT };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int TabBar::max_vertical_scroll_offset() const
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal)
|
|
|
|
|
return 0;
|
|
|
|
|
return max(0, (count() * VERTICAL_TAB_HEIGHT) - height());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::set_vertical_scroll_offset(int offset)
|
|
|
|
|
{
|
|
|
|
|
auto clamped_offset = max(0, min(offset, max_vertical_scroll_offset()));
|
|
|
|
|
if (m_vertical_scroll_offset == clamped_offset)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_vertical_scroll_offset = clamped_offset;
|
|
|
|
|
update_tab_button_geometry();
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabBar::ensure_tab_visible(int index)
|
|
|
|
|
{
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal || index < 0 || index >= count())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto tab_top = index * VERTICAL_TAB_HEIGHT;
|
|
|
|
|
auto tab_bottom = tab_top + VERTICAL_TAB_HEIGHT;
|
|
|
|
|
|
|
|
|
|
if (tab_top < m_vertical_scroll_offset)
|
|
|
|
|
set_vertical_scroll_offset(tab_top);
|
|
|
|
|
else if (tab_bottom > m_vertical_scroll_offset + height())
|
|
|
|
|
set_vertical_scroll_offset(tab_bottom - height());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 05:50:46 -03:00
|
|
|
void TabBar::set_hovered_tab_index(int index)
|
|
|
|
|
{
|
|
|
|
|
if (m_hovered_tab_index == index)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto previous_hovered_tab = m_hovered_tab_index;
|
|
|
|
|
m_hovered_tab_index = index;
|
|
|
|
|
update_tab_button_geometry();
|
|
|
|
|
start_hover_animation(index >= 0 ? index : previous_hovered_tab, index >= 0 ? 1.0 : 0.0);
|
2026-06-10 21:26:41 -03:00
|
|
|
schedule_tab_preview(index);
|
2026-06-01 05:50:46 -03:00
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabBar::update_tab_button_geometry()
|
|
|
|
|
{
|
2026-06-02 11:21:08 -03:00
|
|
|
auto set_button_collapsed_overlay = [](QWidget* button, bool enabled) {
|
|
|
|
|
if (auto* tab_bar_button = qobject_cast<TabBarButton*>(button))
|
|
|
|
|
tab_bar_button->set_collapsed_vertical_overlay(enabled);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto prepare_button = [&](QWidget* button, bool collapsed_overlay) {
|
|
|
|
|
if (!button)
|
|
|
|
|
return;
|
|
|
|
|
button->installEventFilter(this);
|
|
|
|
|
set_button_collapsed_overlay(button, collapsed_overlay);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (tab_layout() == TabLayout::Horizontal) {
|
|
|
|
|
for (int index = 0; index < count(); ++index) {
|
|
|
|
|
prepare_button(tabButton(index, QTabBar::LeftSide), false);
|
|
|
|
|
prepare_button(tabButton(index, QTabBar::RightSide), false);
|
|
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
return;
|
2026-06-02 11:21:08 -03:00
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
|
2026-06-01 05:50:46 -03:00
|
|
|
// Keep this out of paintEvent(): setVisible(), setGeometry(), and raise()
|
|
|
|
|
// dirty child widgets, and doing that while painting can schedule another
|
|
|
|
|
// tab bar repaint before the current one has finished flushing.
|
2026-06-02 11:21:08 -03:00
|
|
|
auto place_expanded_button = [&](int index, QTabBar::ButtonPosition position, QRect shape_rect) {
|
2026-05-31 14:17:47 -03:00
|
|
|
auto* button = tabButton(index, position);
|
|
|
|
|
if (!button)
|
|
|
|
|
return;
|
2026-06-02 11:21:08 -03:00
|
|
|
prepare_button(button, false);
|
|
|
|
|
|
2026-06-01 05:50:46 -03:00
|
|
|
auto should_be_visible = position != QTabBar::RightSide || index == currentIndex() || index == m_hovered_tab_index;
|
|
|
|
|
bool did_update_button = false;
|
|
|
|
|
if (button->isVisible() != should_be_visible) {
|
|
|
|
|
button->setVisible(should_be_visible);
|
|
|
|
|
did_update_button = true;
|
|
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
auto button_size = button->size();
|
|
|
|
|
if (button_size.isEmpty())
|
|
|
|
|
button_size = button->sizeHint();
|
|
|
|
|
if (button_size.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto x = position == QTabBar::RightSide ? shape_rect.right() - button_size.width() - 6 : shape_rect.left() + 6;
|
|
|
|
|
auto y = shape_rect.top() + ((shape_rect.height() - button_size.height()) / 2);
|
2026-06-01 05:50:46 -03:00
|
|
|
QRect button_geometry { { x, y }, button_size };
|
|
|
|
|
if (button->geometry() != button_geometry) {
|
|
|
|
|
button->setGeometry(button_geometry);
|
|
|
|
|
did_update_button = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (did_update_button)
|
|
|
|
|
button->raise();
|
2026-05-31 14:17:47 -03:00
|
|
|
};
|
|
|
|
|
|
2026-06-02 11:21:08 -03:00
|
|
|
auto place_collapsed_button = [&](int index, QTabBar::ButtonPosition position, QRect tab_rect, QRect shape_rect) {
|
|
|
|
|
auto* button = tabButton(index, position);
|
|
|
|
|
if (!button)
|
|
|
|
|
return;
|
|
|
|
|
prepare_button(button, true);
|
|
|
|
|
|
|
|
|
|
auto should_be_visible = position == QTabBar::LeftSide || index == m_hovered_tab_index;
|
|
|
|
|
bool did_update_button = false;
|
|
|
|
|
if (button->isVisible() != should_be_visible) {
|
|
|
|
|
button->setVisible(should_be_visible);
|
|
|
|
|
did_update_button = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto button_size = button->size();
|
|
|
|
|
if (button_size.isEmpty())
|
|
|
|
|
button_size = button->sizeHint();
|
|
|
|
|
if (button_size.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
QPoint button_position;
|
|
|
|
|
if (position == QTabBar::RightSide) {
|
|
|
|
|
button_position = {
|
|
|
|
|
max(tab_rect.left(), shape_rect.left() - 5),
|
|
|
|
|
max(tab_rect.top(), shape_rect.top() - 5),
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
auto x = min(tab_rect.right() - button_size.width() + 1, shape_rect.right() - button_size.width() + 5);
|
|
|
|
|
auto y = min(tab_rect.bottom() - button_size.height() + 1, shape_rect.bottom() - button_size.height() + 5);
|
|
|
|
|
button_position = { x, y };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QRect button_geometry { button_position, button_size };
|
|
|
|
|
if (button->geometry() != button_geometry) {
|
|
|
|
|
button->setGeometry(button_geometry);
|
|
|
|
|
did_update_button = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (did_update_button)
|
|
|
|
|
button->raise();
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
for (int index = 0; index < count(); ++index) {
|
|
|
|
|
auto tab_rect = visual_tab_rect(index);
|
|
|
|
|
if (!tab_rect.isValid())
|
|
|
|
|
continue;
|
|
|
|
|
|
2026-06-02 11:21:08 -03:00
|
|
|
if (tab_layout() == TabLayout::VerticalCollapsed) {
|
|
|
|
|
auto shape_rect = collapsed_vertical_tab_shape_rect(tab_rect);
|
|
|
|
|
place_collapsed_button(index, QTabBar::LeftSide, tab_rect, shape_rect);
|
|
|
|
|
place_collapsed_button(index, QTabBar::RightSide, tab_rect, shape_rect);
|
|
|
|
|
} else {
|
2026-06-02 12:45:44 -03:00
|
|
|
auto shape_rect = tab_card_shape_rect(tab_rect);
|
2026-06-02 11:21:08 -03:00
|
|
|
place_expanded_button(index, QTabBar::LeftSide, shape_rect);
|
|
|
|
|
place_expanded_button(index, QTabBar::RightSide, shape_rect);
|
|
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
void TabBar::toggle_window_maximized()
|
|
|
|
|
{
|
|
|
|
|
auto* top_level_window = window();
|
|
|
|
|
if (top_level_window->isMaximized())
|
|
|
|
|
top_level_window->showNormal();
|
|
|
|
|
else
|
|
|
|
|
top_level_window->showMaximized();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
bool TabBar::start_window_move()
|
|
|
|
|
{
|
|
|
|
|
auto* handle = window()->windowHandle();
|
|
|
|
|
if (!handle)
|
|
|
|
|
return false;
|
2026-06-02 12:45:44 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
if (start_appkit_window_drag(*this))
|
|
|
|
|
return true;
|
|
|
|
|
#endif
|
2026-05-31 19:37:00 -03:00
|
|
|
return handle->startSystemMove();
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-02 22:38:01 -03:00
|
|
|
TabWidget::TabWidget(QWidget* parent)
|
2026-03-17 08:38:25 -03:00
|
|
|
: QWidget(parent)
|
|
|
|
|
{
|
2026-06-01 10:46:39 -03:00
|
|
|
if (auto* top_level_window = window(); top_level_window != this)
|
|
|
|
|
top_level_window->installEventFilter(this);
|
|
|
|
|
|
2026-06-03 14:59:02 -03:00
|
|
|
m_vertical_tabs_expanded_width = Application::settings().tab_settings().vertical_tabs_expanded_width.value_or(VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH);
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tabs_expanded_width = clamp_vertical_tabs_expanded_width(m_vertical_tabs_expanded_width);
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
m_tab_bar = new TabBar(this);
|
2026-05-25 16:22:07 -03:00
|
|
|
setAcceptDrops(true);
|
2026-05-19 16:29:50 -03:00
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
m_tab_bar->setDocumentMode(true);
|
|
|
|
|
m_tab_bar->setElideMode(Qt::TextElideMode::ElideRight);
|
2026-05-25 16:22:07 -03:00
|
|
|
m_tab_bar->setMovable(false);
|
2026-03-17 08:38:25 -03:00
|
|
|
m_tab_bar->setTabsClosable(true);
|
|
|
|
|
m_tab_bar->setExpanding(false);
|
|
|
|
|
m_tab_bar->setUsesScrollButtons(true);
|
|
|
|
|
m_tab_bar->setDrawBase(false);
|
2026-06-01 10:46:39 -03:00
|
|
|
m_tab_bar->installEventFilter(this);
|
2026-03-17 08:38:25 -03:00
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
m_toolbar_container = new QStackedWidget(this);
|
|
|
|
|
m_toolbar_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
2026-06-03 05:19:56 -03:00
|
|
|
m_toolbar_container->installEventFilter(this);
|
2026-03-17 08:38:25 -03:00
|
|
|
m_stacked_widget = new QStackedWidget(this);
|
2026-06-02 15:22:48 -03:00
|
|
|
m_page_column = new QWidget(this);
|
|
|
|
|
m_page_column_layout = new QVBoxLayout(m_page_column);
|
|
|
|
|
m_page_column_layout->setSpacing(0);
|
|
|
|
|
m_page_column_layout->setContentsMargins(0, 0, 0, 0);
|
2026-03-17 08:38:25 -03:00
|
|
|
|
2026-06-01 05:06:15 -03:00
|
|
|
m_new_tab_button = new NewTabButton(*m_tab_bar, this);
|
2026-05-25 16:21:50 -03:00
|
|
|
m_new_tab_button->setObjectName("LadybirdNewTabButton");
|
2026-05-31 12:59:53 -03:00
|
|
|
m_new_tab_button->setIconSize(QSize(18, 18));
|
2026-05-25 16:22:32 -03:00
|
|
|
m_new_tab_button->setFixedSize(32, 32);
|
|
|
|
|
m_new_tab_button->setFocusPolicy(Qt::NoFocus);
|
2026-03-17 08:38:25 -03:00
|
|
|
m_new_tab_button->setToolTip("New Tab");
|
2026-06-01 10:46:39 -03:00
|
|
|
m_new_tab_button->installEventFilter(this);
|
2026-03-17 08:38:25 -03:00
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
auto window_control_buttons = create_window_control_buttons(*this, "LadybirdTabStripWindowControls", { 18, 18 }, { 40, 40 });
|
|
|
|
|
m_window_controls = window_control_buttons.container;
|
|
|
|
|
m_minimize_window_button = window_control_buttons.minimize;
|
|
|
|
|
m_maximize_window_button = window_control_buttons.maximize;
|
|
|
|
|
m_close_window_button = window_control_buttons.close;
|
2026-05-25 16:21:50 -03:00
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
recreate_icons();
|
|
|
|
|
|
|
|
|
|
m_tab_bar_row = new QWidget(this);
|
2026-05-25 16:21:50 -03:00
|
|
|
m_tab_bar_row->setObjectName("LadybirdTabStrip");
|
2026-05-31 14:17:47 -03:00
|
|
|
m_tab_bar_row_layout = new QHBoxLayout(m_tab_bar_row);
|
2026-05-25 16:21:50 -03:00
|
|
|
m_tab_bar_row->installEventFilter(this);
|
2026-03-17 08:38:25 -03:00
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tabs_content = new QWidget(this);
|
|
|
|
|
m_vertical_tabs_content_layout = new QHBoxLayout(m_vertical_tabs_content);
|
|
|
|
|
m_vertical_tabs_content_layout->setSpacing(0);
|
|
|
|
|
m_vertical_tabs_content_layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
m_vertical_tabs_content->installEventFilter(this);
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column = new QWidget(this);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_vertical_tab_bar_column->setObjectName("LadybirdVerticalTabBar");
|
2026-06-02 15:56:03 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column->setAttribute(Qt::WA_NativeWindow);
|
2026-06-02 15:56:03 -03:00
|
|
|
#endif
|
2026-05-31 14:17:47 -03:00
|
|
|
m_vertical_tab_bar_column_layout = new QVBoxLayout(m_vertical_tab_bar_column);
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tab_bar_column->setProperty(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, false);
|
|
|
|
|
m_vertical_tab_bar_column->setProperty(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false);
|
|
|
|
|
m_vertical_tab_bar_column->installEventFilter(this);
|
2026-05-31 14:17:47 -03:00
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_content_separator = new QWidget(this);
|
|
|
|
|
m_vertical_tabs_content_separator->setObjectName("LadybirdVerticalTabsContentSeparator");
|
|
|
|
|
m_vertical_tabs_content_separator->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
|
|
|
m_vertical_tabs_content_separator->hide();
|
|
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tabs_reserved_space = new QWidget(m_vertical_tabs_content);
|
|
|
|
|
m_vertical_tabs_reserved_space->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
|
2026-05-31 14:17:47 -03:00
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_resize_handle = new QWidget(this);
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tabs_resize_handle->setObjectName("LadybirdVerticalTabsResizeHandle");
|
2026-06-02 15:56:03 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_resize_handle->setAttribute(Qt::WA_NativeWindow);
|
2026-06-02 15:56:03 -03:00
|
|
|
#endif
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tabs_resize_handle->setCursor(Qt::SizeHorCursor);
|
|
|
|
|
m_vertical_tabs_resize_handle->installEventFilter(this);
|
|
|
|
|
m_vertical_tabs_resize_handle->hide();
|
|
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tabs_hover_collapse_timer = new QTimer(this);
|
|
|
|
|
m_vertical_tabs_hover_collapse_timer->setInterval(VERTICAL_TABS_HOVER_COLLAPSE_POLL_INTERVAL_MS);
|
|
|
|
|
connect(m_vertical_tabs_hover_collapse_timer, &QTimer::timeout, this, [this]() {
|
|
|
|
|
update_vertical_tabs_hover_expanded();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
m_main_layout = new QVBoxLayout(this);
|
|
|
|
|
m_main_layout->setSpacing(0);
|
|
|
|
|
m_main_layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
rebuild_layout();
|
2026-05-25 16:21:50 -03:00
|
|
|
update_chrome_style();
|
2026-03-17 08:38:25 -03:00
|
|
|
|
|
|
|
|
connect(m_tab_bar, &QTabBar::currentChanged, this, [this](int index) {
|
|
|
|
|
if (index >= 0 && index < m_stacked_widget->count())
|
|
|
|
|
m_stacked_widget->setCurrentIndex(index);
|
|
|
|
|
|
|
|
|
|
emit current_tab_changed(index);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_toolbar_container->setCurrentIndex(index);
|
|
|
|
|
update_vertical_tabs_overlay_geometry();
|
|
|
|
|
update_vertical_tabs_resize_handle();
|
2026-03-17 08:38:25 -03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connect(m_tab_bar, &QTabBar::tabCloseRequested, this, &TabWidget::tab_close_requested);
|
|
|
|
|
|
|
|
|
|
connect(m_tab_bar, &QTabBar::tabMoved, this, [this](int from, int to) {
|
|
|
|
|
ScopeGuard guard { [&]() { m_stacked_widget->blockSignals(false); } };
|
|
|
|
|
m_stacked_widget->blockSignals(true);
|
|
|
|
|
|
|
|
|
|
auto* widget = m_stacked_widget->widget(from);
|
|
|
|
|
m_stacked_widget->removeWidget(widget);
|
|
|
|
|
m_stacked_widget->insertWidget(to, widget);
|
2026-06-02 12:45:44 -03:00
|
|
|
|
|
|
|
|
auto* toolbar = as<Tab>(widget)->toolbar_container();
|
|
|
|
|
if (auto toolbar_index = m_toolbar_container->indexOf(toolbar); toolbar_index != -1) {
|
|
|
|
|
m_toolbar_container->removeWidget(toolbar);
|
|
|
|
|
m_toolbar_container->insertWidget(to, toolbar);
|
|
|
|
|
}
|
2026-03-17 08:38:25 -03:00
|
|
|
m_stacked_widget->setCurrentIndex(m_tab_bar->currentIndex());
|
|
|
|
|
});
|
2026-05-25 16:21:50 -03:00
|
|
|
|
|
|
|
|
connect(m_minimize_window_button, &QToolButton::clicked, this, [this] {
|
|
|
|
|
window()->showMinimized();
|
|
|
|
|
});
|
|
|
|
|
connect(m_maximize_window_button, &QToolButton::clicked, this, [this] {
|
|
|
|
|
toggle_window_maximized();
|
|
|
|
|
});
|
|
|
|
|
connect(m_close_window_button, &QToolButton::clicked, this, [this] {
|
|
|
|
|
window()->close();
|
|
|
|
|
});
|
2026-03-17 08:38:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::add_tab(Tab* widget, QString const& label)
|
|
|
|
|
{
|
2026-05-25 16:22:07 -03:00
|
|
|
insert_tab(m_tab_bar->count(), widget, label);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::insert_tab(int index, Tab* widget, QString const& label)
|
|
|
|
|
{
|
|
|
|
|
m_stacked_widget->insertWidget(index, widget);
|
|
|
|
|
m_tab_bar->insertTab(index, label);
|
2026-06-02 15:22:48 -03:00
|
|
|
widget->set_toolbar_container_in_tab_layout(false);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_toolbar_container->insertWidget(index, widget->toolbar_container());
|
2026-03-17 08:38:25 -03:00
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
widget->set_vertical_tabs_enabled(m_vertical_tabs_enabled);
|
2026-05-31 19:35:03 -03:00
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
update_toolbar_placement();
|
2026-03-17 08:38:25 -03:00
|
|
|
update_tab_layout();
|
2026-05-31 14:17:47 -03:00
|
|
|
update_tab_button_visibility();
|
2026-03-17 08:38:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::remove_tab(int index)
|
2026-05-25 16:22:07 -03:00
|
|
|
{
|
|
|
|
|
take_tab(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Tab* TabWidget::take_tab(int index)
|
2024-04-02 22:38:01 -03:00
|
|
|
{
|
2026-03-17 08:38:25 -03:00
|
|
|
auto* widget = m_stacked_widget->widget(index);
|
2026-05-25 16:22:07 -03:00
|
|
|
if (!widget)
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
m_stacked_widget->removeWidget(widget);
|
2026-06-02 12:45:44 -03:00
|
|
|
auto* toolbar = as<Tab>(widget)->toolbar_container();
|
|
|
|
|
if (m_toolbar_container->indexOf(toolbar) != -1)
|
|
|
|
|
m_toolbar_container->removeWidget(toolbar);
|
|
|
|
|
as<Tab>(widget)->set_toolbar_container_in_tab_layout(true);
|
2026-03-17 08:38:25 -03:00
|
|
|
|
|
|
|
|
m_tab_bar->removeTab(index);
|
2024-04-02 22:38:01 -03:00
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
if (m_tab_bar->count() > 0 && m_tab_bar->currentIndex() >= 0)
|
|
|
|
|
m_stacked_widget->setCurrentIndex(m_tab_bar->currentIndex());
|
|
|
|
|
|
|
|
|
|
update_tab_layout();
|
2026-05-31 14:17:47 -03:00
|
|
|
update_tab_button_visibility();
|
2026-05-25 16:22:07 -03:00
|
|
|
return as<Tab>(widget);
|
2026-03-17 08:38:25 -03:00
|
|
|
}
|
2024-04-02 22:38:01 -03:00
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
void TabWidget::set_current_tab(Tab* widget)
|
|
|
|
|
{
|
|
|
|
|
if (auto index = m_stacked_widget->indexOf(widget); index >= 0)
|
|
|
|
|
set_current_index(index);
|
|
|
|
|
}
|
2024-07-26 07:38:26 -03:00
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
int TabWidget::index_of(Tab* widget) const
|
|
|
|
|
{
|
|
|
|
|
return m_stacked_widget->indexOf(widget);
|
2024-04-02 22:38:01 -03:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
void TabWidget::set_new_tab_action(QAction* action)
|
2024-07-26 10:39:26 -03:00
|
|
|
{
|
2026-03-17 08:38:25 -03:00
|
|
|
disconnect(m_new_tab_button, &QToolButton::clicked, nullptr, nullptr);
|
2024-07-26 10:39:26 -03:00
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
if (!action)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
connect(m_new_tab_button, &QToolButton::clicked, action, &QAction::trigger);
|
|
|
|
|
}
|
2024-07-26 10:39:26 -03:00
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabWidget::set_tab_bar_visible(bool visible)
|
|
|
|
|
{
|
|
|
|
|
if (m_tab_bar_visible == visible)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_tab_bar_visible = visible;
|
|
|
|
|
update_tab_chrome_visibility();
|
2026-06-03 13:27:18 -03:00
|
|
|
update_tab_layout();
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 13:18:10 -03:00
|
|
|
void TabWidget::set_window_controls_visible(bool visible)
|
|
|
|
|
{
|
2026-05-31 14:17:47 -03:00
|
|
|
m_window_controls_visible = visible;
|
2026-06-02 12:45:44 -03:00
|
|
|
update_tab_toolbar_window_controls_visibility();
|
2026-05-31 14:17:47 -03:00
|
|
|
update_tab_chrome_visibility();
|
2026-05-27 13:18:10 -03:00
|
|
|
update_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabWidget::set_vertical_tabs_enabled(bool enabled)
|
|
|
|
|
{
|
|
|
|
|
if (m_vertical_tabs_enabled == enabled)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_vertical_tabs_enabled = enabled;
|
2026-06-01 10:46:39 -03:00
|
|
|
if (!m_vertical_tabs_enabled)
|
|
|
|
|
set_vertical_tabs_hover_expanded(false);
|
2026-05-31 19:35:03 -03:00
|
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index) {
|
2026-05-31 19:37:00 -03:00
|
|
|
tab(index)->set_vertical_tabs_enabled(enabled);
|
2026-05-31 19:35:03 -03:00
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
rebuild_layout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::set_vertical_tabs_expanded(bool expanded)
|
|
|
|
|
{
|
|
|
|
|
if (m_vertical_tabs_expanded == expanded)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_vertical_tabs_expanded = expanded;
|
2026-06-01 10:46:39 -03:00
|
|
|
set_vertical_tabs_hover_expanded(false);
|
|
|
|
|
rebuild_layout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::set_vertical_tabs_expand_on_hover(bool expand_on_hover)
|
|
|
|
|
{
|
|
|
|
|
if (m_vertical_tabs_expand_on_hover == expand_on_hover)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_vertical_tabs_expand_on_hover = expand_on_hover;
|
|
|
|
|
if (!m_vertical_tabs_expand_on_hover)
|
|
|
|
|
set_vertical_tabs_hover_expanded(false);
|
2026-05-31 14:17:47 -03:00
|
|
|
rebuild_layout();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
bool TabWidget::event(QEvent* event)
|
|
|
|
|
{
|
2026-05-29 08:34:52 -03:00
|
|
|
if (auto type = event->type(); type == QEvent::PaletteChange) {
|
2026-03-17 08:38:25 -03:00
|
|
|
recreate_icons();
|
2026-05-25 16:21:50 -03:00
|
|
|
update_chrome_style();
|
|
|
|
|
} else if (type == QEvent::WindowStateChange) {
|
|
|
|
|
update_window_button_icons();
|
2026-06-01 10:46:39 -03:00
|
|
|
} else if (type == QEvent::Leave) {
|
|
|
|
|
defer_update_vertical_tabs_hover_expanded();
|
2024-07-26 10:39:26 -03:00
|
|
|
}
|
2026-03-17 08:38:25 -03:00
|
|
|
|
|
|
|
|
return QWidget::event(event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
void TabWidget::dragEnterEvent(QDragEnterEvent* event)
|
|
|
|
|
{
|
|
|
|
|
accept_tab_drag(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::dragMoveEvent(QDragMoveEvent* event)
|
|
|
|
|
{
|
|
|
|
|
accept_tab_drag(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::dropEvent(QDropEvent* event)
|
|
|
|
|
{
|
|
|
|
|
accept_tab_drop(event, m_tab_bar->count());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
bool TabWidget::eventFilter(QObject* watched, QEvent* event)
|
|
|
|
|
{
|
2026-06-01 10:46:39 -03:00
|
|
|
if (watched == window() && event->type() == QEvent::Leave)
|
2026-06-02 12:45:44 -03:00
|
|
|
defer_update_vertical_tabs_hover_expanded();
|
2026-06-01 10:46:39 -03:00
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
if (watched == m_vertical_tabs_resize_handle) {
|
|
|
|
|
auto reset_resize_handle = [this] {
|
|
|
|
|
m_is_resizing_vertical_tabs = false;
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_resize_handle->releaseMouse();
|
|
|
|
|
QApplication::restoreOverrideCursor();
|
2026-05-31 19:37:00 -03:00
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (event->type() == QEvent::Enter) {
|
|
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, true);
|
|
|
|
|
} else if (event->type() == QEvent::Leave) {
|
|
|
|
|
if (!m_is_resizing_vertical_tabs)
|
|
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, false);
|
|
|
|
|
} else if (event->type() == QEvent::MouseButtonDblClick) {
|
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
|
|
|
if (mouse_event->button() == Qt::LeftButton && m_vertical_tabs_expanded) {
|
|
|
|
|
apply_vertical_tabs_expanded_width(VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH);
|
|
|
|
|
persist_vertical_tabs_expanded_width();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} else if (event->type() == QEvent::MouseButtonPress) {
|
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
|
|
|
if (mouse_event->button() != Qt::LeftButton)
|
|
|
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
|
|
|
|
|
|
m_is_resizing_vertical_tabs = true;
|
|
|
|
|
m_vertical_tabs_resize_start_global_x = mouse_event->globalPosition().toPoint().x();
|
|
|
|
|
m_vertical_tabs_resize_start_width = m_vertical_tabs_expanded_width;
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_resize_handle->grabMouse();
|
|
|
|
|
QApplication::setOverrideCursor(Qt::SizeHorCursor);
|
2026-05-31 19:37:00 -03:00
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, true);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (event->type() == QEvent::MouseMove) {
|
|
|
|
|
if (!m_is_resizing_vertical_tabs)
|
|
|
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
|
|
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
|
|
|
auto delta = mouse_event->globalPosition().toPoint().x() - m_vertical_tabs_resize_start_global_x;
|
|
|
|
|
apply_vertical_tabs_expanded_width(m_vertical_tabs_resize_start_width + delta);
|
|
|
|
|
return true;
|
|
|
|
|
} else if (event->type() == QEvent::MouseButtonRelease) {
|
|
|
|
|
if (!m_is_resizing_vertical_tabs)
|
|
|
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
|
|
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
|
|
|
|
if (mouse_event->button() != Qt::LeftButton)
|
|
|
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
|
|
|
|
|
|
persist_vertical_tabs_expanded_width();
|
|
|
|
|
reset_resize_handle();
|
|
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, m_vertical_tabs_resize_handle->underMouse());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
if (watched == m_vertical_tabs_content && event->type() == QEvent::Leave)
|
|
|
|
|
defer_update_vertical_tabs_hover_expanded();
|
|
|
|
|
|
2026-06-03 05:19:56 -03:00
|
|
|
if (watched == m_toolbar_container && event->type() == QEvent::Resize)
|
|
|
|
|
update_tab_layout();
|
|
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
auto is_vertical_tabs_hover_target = watched == m_vertical_tab_bar_column
|
|
|
|
|
|| watched == m_tab_bar
|
2026-06-02 15:22:48 -03:00
|
|
|
|| watched == m_new_tab_button;
|
2026-06-01 10:46:39 -03:00
|
|
|
|
|
|
|
|
if (is_vertical_tabs_hover_target) {
|
|
|
|
|
if (event->type() == QEvent::Enter) {
|
|
|
|
|
set_vertical_tabs_hover_expanded(true);
|
|
|
|
|
} else if (event->type() == QEvent::Leave) {
|
|
|
|
|
defer_update_vertical_tabs_hover_expanded();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 10:57:15 -03:00
|
|
|
if ((watched == m_tab_bar_row || watched == m_vertical_tab_bar_column) && window_uses_client_side_decorations(*this)) {
|
2026-05-31 19:37:00 -03:00
|
|
|
auto is_empty_chrome_area = [this, watched](QMouseEvent const& mouse_event) {
|
|
|
|
|
if (watched == m_vertical_tab_bar_column) {
|
|
|
|
|
auto* child = m_vertical_tab_bar_column->childAt(mouse_event.pos());
|
2026-06-02 15:22:48 -03:00
|
|
|
return child == nullptr;
|
2026-05-31 19:37:00 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
return m_tab_bar_row->childAt(mouse_event.pos()) == nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
if (event->type() == QEvent::MouseButtonDblClick) {
|
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
2026-05-31 19:37:00 -03:00
|
|
|
if (mouse_event->button() == Qt::LeftButton && is_empty_chrome_area(*mouse_event)) {
|
2026-05-25 16:21:50 -03:00
|
|
|
toggle_window_maximized();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
|
|
|
|
auto* mouse_event = static_cast<QMouseEvent*>(event);
|
2026-05-31 19:37:00 -03:00
|
|
|
if (mouse_event->button() == Qt::LeftButton && is_empty_chrome_area(*mouse_event) && start_window_move())
|
2026-05-25 16:21:50 -03:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return QWidget::eventFilter(watched, event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabWidget::resizeEvent(QResizeEvent* event)
|
|
|
|
|
{
|
|
|
|
|
QWidget::resizeEvent(event);
|
|
|
|
|
update_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
void TabWidget::accept_tab_drag(QDragMoveEvent* event)
|
|
|
|
|
{
|
|
|
|
|
if (!s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
|
|
|
event->ignore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
auto* drag_area = tab_drag_area_widget();
|
|
|
|
|
auto position_in_drag_area = drag_area->mapFrom(this, event->position().toPoint());
|
|
|
|
|
if (!drag_area->rect().contains(position_in_drag_area)) {
|
2026-05-25 21:12:55 -03:00
|
|
|
event->ignore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:22:07 -03:00
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::accept_tab_drop(QDropEvent* event, int index)
|
|
|
|
|
{
|
|
|
|
|
if (!s_active_tab_drag_source || !event->mimeData()->hasFormat(LADYBIRD_TAB_MIME_TYPE)) {
|
|
|
|
|
event->ignore();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_pending_tab_drop_target = this;
|
|
|
|
|
s_pending_tab_drop_index = index;
|
|
|
|
|
event->setDropAction(Qt::MoveAction);
|
|
|
|
|
event->accept();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
QRect TabWidget::tab_strip_global_rect() const
|
2026-03-17 08:38:25 -03:00
|
|
|
{
|
2026-05-31 14:17:47 -03:00
|
|
|
if (auto* widget = tab_drag_area_widget())
|
|
|
|
|
return { widget->mapToGlobal(QPoint(0, 0)), widget->size() };
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TabLayout TabWidget::current_tab_layout() const
|
|
|
|
|
{
|
|
|
|
|
if (!m_vertical_tabs_enabled)
|
|
|
|
|
return TabLayout::Horizontal;
|
2026-06-01 10:46:39 -03:00
|
|
|
return vertical_tabs_effectively_expanded() ? TabLayout::VerticalExpanded : TabLayout::VerticalCollapsed;
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QWidget* TabWidget::tab_drag_area_widget() const
|
|
|
|
|
{
|
|
|
|
|
return m_tab_bar->tab_layout() == TabLayout::Horizontal ? m_tab_bar_row : m_vertical_tab_bar_column;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
bool TabWidget::vertical_tabs_effectively_expanded() const
|
|
|
|
|
{
|
|
|
|
|
return m_vertical_tabs_expanded || m_vertical_tabs_hover_expanded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TabWidget::can_expand_vertical_tabs_on_hover() const
|
|
|
|
|
{
|
|
|
|
|
return m_vertical_tabs_enabled && m_vertical_tabs_expand_on_hover && !m_vertical_tabs_expanded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TabWidget::cursor_is_over_vertical_tabs() const
|
|
|
|
|
{
|
2026-06-02 12:45:44 -03:00
|
|
|
if (!m_vertical_tabs_content->isVisible())
|
2026-06-01 10:46:39 -03:00
|
|
|
return false;
|
|
|
|
|
|
2026-06-04 16:18:54 -03:00
|
|
|
if (m_vertical_tab_bar_column->underMouse() || m_tab_bar->underMouse() || m_new_tab_button->underMouse())
|
|
|
|
|
return true;
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
auto vertical_tabs_rect = QRect {
|
|
|
|
|
m_vertical_tabs_content->mapToGlobal(QPoint { 0, 0 }),
|
|
|
|
|
QSize { current_vertical_tabs_width(), m_vertical_tabs_content->height() }
|
|
|
|
|
};
|
2026-06-04 16:18:54 -03:00
|
|
|
return window()->underMouse() && vertical_tabs_rect.contains(QCursor::pos());
|
2026-06-01 10:46:39 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int TabWidget::vertical_tabs_layout_width() const
|
|
|
|
|
{
|
2026-06-03 13:27:18 -03:00
|
|
|
if (!m_tab_bar_visible)
|
|
|
|
|
return 0;
|
2026-06-01 10:46:39 -03:00
|
|
|
return m_vertical_tabs_expanded ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
bool TabWidget::should_show_window_controls_in_tab_toolbar() const
|
|
|
|
|
{
|
2026-06-02 16:10:01 -03:00
|
|
|
return m_window_controls_visible && m_tab_bar->tab_layout() != TabLayout::Horizontal;
|
2026-06-02 12:45:44 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_toolbar_placement()
|
|
|
|
|
{
|
2026-06-02 16:10:01 -03:00
|
|
|
auto use_full_width_toolbar = current_tab_layout() != TabLayout::Horizontal;
|
2026-06-02 12:45:44 -03:00
|
|
|
|
|
|
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index) {
|
|
|
|
|
auto* current_tab = tab(index);
|
|
|
|
|
auto* toolbar = current_tab->toolbar_container();
|
2026-06-02 15:34:12 -03:00
|
|
|
set_dynamic_property_if_needed(*toolbar, FULL_WIDTH_TOOLBAR_PROPERTY, use_full_width_toolbar);
|
2026-06-02 12:45:44 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_toolbar_container->setCurrentIndex(m_tab_bar->currentIndex());
|
|
|
|
|
update_tab_toolbar_window_controls_visibility();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_tab_toolbar_window_controls_visibility()
|
|
|
|
|
{
|
|
|
|
|
auto show_controls = should_show_window_controls_in_tab_toolbar();
|
|
|
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index)
|
|
|
|
|
tab(index)->set_toolbar_window_controls_visible(show_controls);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabWidget::rebuild_layout()
|
|
|
|
|
{
|
|
|
|
|
clear_layout(*m_main_layout);
|
|
|
|
|
clear_layout(*m_tab_bar_row_layout);
|
2026-06-02 15:22:48 -03:00
|
|
|
clear_layout(*m_page_column_layout);
|
2026-05-31 14:17:47 -03:00
|
|
|
clear_layout(*m_vertical_tab_bar_column_layout);
|
|
|
|
|
clear_layout(*m_vertical_tabs_content_layout);
|
|
|
|
|
|
|
|
|
|
m_tab_bar->set_tab_layout(current_tab_layout());
|
2026-06-02 12:45:44 -03:00
|
|
|
update_toolbar_placement();
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
|
|
|
|
|
rebuild_layout_for_vertical_tabs();
|
2026-06-02 16:10:01 -03:00
|
|
|
m_main_layout->addWidget(m_toolbar_container);
|
|
|
|
|
m_page_column->hide();
|
2026-06-02 15:22:48 -03:00
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tabs_content_layout->addWidget(m_vertical_tabs_reserved_space);
|
2026-06-02 16:10:01 -03:00
|
|
|
m_vertical_tabs_content_layout->addWidget(m_stacked_widget, 1);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_main_layout->addWidget(m_vertical_tabs_content, 1);
|
|
|
|
|
m_vertical_tabs_content->show();
|
|
|
|
|
} else {
|
|
|
|
|
rebuild_layout_for_horizontal_tabs();
|
2026-06-02 15:22:48 -03:00
|
|
|
rebuild_page_column();
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
m_main_layout->addWidget(m_tab_bar_row);
|
2026-06-02 15:22:48 -03:00
|
|
|
m_main_layout->addWidget(m_page_column, 1);
|
|
|
|
|
m_page_column->show();
|
2026-05-31 14:17:47 -03:00
|
|
|
m_vertical_tabs_content->hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update_tab_button_visibility();
|
|
|
|
|
update_tab_chrome_visibility();
|
2026-03-17 08:38:25 -03:00
|
|
|
update_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabWidget::rebuild_layout_for_horizontal_tabs()
|
|
|
|
|
{
|
|
|
|
|
m_tab_bar_row->setMinimumHeight(HORIZONTAL_TAB_STRIP_HEIGHT);
|
2026-06-02 16:24:13 -03:00
|
|
|
m_tab_bar_row_layout->setSpacing(0);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_tab_bar_row_layout->setContentsMargins(12, 2, 4, 1);
|
|
|
|
|
|
|
|
|
|
m_new_tab_button->setText({});
|
2026-06-02 15:34:12 -03:00
|
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_EXPANDED_PROPERTY, false);
|
2026-06-02 16:19:00 -03:00
|
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_BUTTON_PROPERTY, true);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_new_tab_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
|
2026-06-02 16:24:13 -03:00
|
|
|
m_new_tab_button->setFixedSize(HORIZONTAL_TAB_HEIGHT, HORIZONTAL_TAB_HEIGHT);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
if (use_left_traffic_light_window_controls()) {
|
|
|
|
|
m_tab_bar_row_layout->addWidget(m_window_controls, 0, Qt::AlignVCenter);
|
2026-06-02 16:24:13 -03:00
|
|
|
m_tab_bar_row_layout->addSpacing(16);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_tab_bar_row_layout->addWidget(m_tab_bar);
|
|
|
|
|
m_tab_bar_row_layout->addWidget(m_new_tab_button, 0, Qt::AlignVCenter);
|
|
|
|
|
m_tab_bar_row_layout->addStretch(1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
m_tab_bar_row_layout->addWidget(m_tab_bar);
|
|
|
|
|
m_tab_bar_row_layout->addWidget(m_new_tab_button, 0, Qt::AlignVCenter);
|
|
|
|
|
m_tab_bar_row_layout->addStretch(1);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_tab_bar_row_layout->addWidget(m_window_controls);
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::rebuild_layout_for_vertical_tabs()
|
|
|
|
|
{
|
2026-06-01 10:46:39 -03:00
|
|
|
auto reserved_width = vertical_tabs_layout_width();
|
2026-05-31 19:37:00 -03:00
|
|
|
auto side_bar_width = current_vertical_tabs_width();
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tabs_reserved_space->setFixedWidth(reserved_width);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_vertical_tab_bar_column->setFixedWidth(side_bar_width);
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column_layout->setSpacing(0);
|
|
|
|
|
auto tab_layout = m_tab_bar->tab_layout();
|
2026-06-02 15:22:48 -03:00
|
|
|
auto side_margin = vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed);
|
|
|
|
|
m_window_controls->layout()->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
m_vertical_tab_bar_column_layout->setContentsMargins(side_margin, VERTICAL_TABS_TOP_MARGIN, side_margin, 8);
|
2026-06-02 12:45:44 -03:00
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
update_vertical_tabs_button_layout();
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
m_vertical_tab_bar_column_layout->addWidget(m_tab_bar);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column_layout->addWidget(m_new_tab_button);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_vertical_tab_bar_column_layout->addStretch(1);
|
2026-05-31 19:37:00 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
void TabWidget::rebuild_page_column()
|
|
|
|
|
{
|
|
|
|
|
m_page_column_layout->addWidget(m_toolbar_container);
|
|
|
|
|
m_page_column_layout->addWidget(m_stacked_widget, 1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
int TabWidget::current_vertical_tabs_width() const
|
|
|
|
|
{
|
2026-06-01 10:46:39 -03:00
|
|
|
return vertical_tabs_effectively_expanded() ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH;
|
2026-05-31 19:37:00 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::apply_vertical_tabs_expanded_width(int width)
|
|
|
|
|
{
|
|
|
|
|
auto clamped_width = clamp_vertical_tabs_expanded_width(width);
|
|
|
|
|
if (m_vertical_tabs_expanded_width == clamped_width)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_vertical_tabs_expanded_width = clamped_width;
|
|
|
|
|
update_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::persist_vertical_tabs_expanded_width()
|
|
|
|
|
{
|
2026-06-03 14:59:02 -03:00
|
|
|
auto tab_settings = Application::settings().tab_settings();
|
|
|
|
|
|
|
|
|
|
using ValueType = decltype(tab_settings.vertical_tabs_expanded_width)::ValueType;
|
|
|
|
|
tab_settings.vertical_tabs_expanded_width = clamp(m_vertical_tabs_expanded_width, 0, NumericLimits<ValueType>::max());
|
|
|
|
|
|
|
|
|
|
Application::settings().set_tab_settings(tab_settings);
|
2026-05-31 19:37:00 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::set_resize_handle_property(char const* property, bool enabled)
|
|
|
|
|
{
|
2026-06-02 15:34:12 -03:00
|
|
|
set_dynamic_property_if_needed(*m_vertical_tab_bar_column, property, enabled);
|
2026-05-31 19:37:00 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_vertical_tabs_resize_handle()
|
|
|
|
|
{
|
|
|
|
|
auto is_vertical = m_tab_bar->tab_layout() != TabLayout::Horizontal;
|
2026-06-03 13:27:18 -03:00
|
|
|
auto show_resize_handle = m_tab_bar_visible && is_vertical && m_vertical_tabs_expanded;
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tabs_resize_handle->setVisible(show_resize_handle);
|
|
|
|
|
if (!show_resize_handle) {
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_resize_handle->releaseMouse();
|
|
|
|
|
if (m_is_resizing_vertical_tabs)
|
|
|
|
|
QApplication::restoreOverrideCursor();
|
2026-05-31 19:37:00 -03:00
|
|
|
m_is_resizing_vertical_tabs = false;
|
|
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY, false);
|
|
|
|
|
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto handle_width = VERTICAL_TABS_RESIZE_HIT_AREA_WIDTH;
|
2026-06-01 10:46:39 -03:00
|
|
|
auto divider_x = vertical_tabs_layout_width() - 1;
|
2026-06-02 15:56:24 -03:00
|
|
|
auto chrome_rect = vertical_tabs_chrome_rect();
|
|
|
|
|
m_vertical_tabs_resize_handle->setGeometry(
|
|
|
|
|
divider_x - (handle_width / 2),
|
|
|
|
|
chrome_rect.y(),
|
|
|
|
|
handle_width,
|
|
|
|
|
chrome_rect.height());
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tabs_resize_handle->raise();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
void TabWidget::update_vertical_tabs_content_separator()
|
|
|
|
|
{
|
2026-06-02 16:10:01 -03:00
|
|
|
auto show_content_separator = m_tab_bar_visible && m_tab_bar->tab_layout() != TabLayout::Horizontal;
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_content_separator->setVisible(show_content_separator);
|
|
|
|
|
if (!show_content_separator)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto separator_x = max(0, current_vertical_tabs_width() - 1);
|
2026-06-03 05:19:56 -03:00
|
|
|
auto separator_y = max(0, m_toolbar_container->height() - 1);
|
|
|
|
|
m_vertical_tabs_content_separator->setGeometry(separator_x, separator_y, max(0, width() - separator_x), 1);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_content_separator->raise();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
void TabWidget::update_vertical_tabs_action_labels()
|
|
|
|
|
{
|
2026-06-02 15:34:12 -03:00
|
|
|
auto text = vertical_tabs_effectively_expanded() ? "New Tab" : QString {};
|
|
|
|
|
if (m_new_tab_button->text() != text)
|
|
|
|
|
m_new_tab_button->setText(text);
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 12:45:44 -03:00
|
|
|
void TabWidget::update_vertical_tabs_hover_layout()
|
|
|
|
|
{
|
|
|
|
|
m_tab_bar->set_tab_layout(current_tab_layout());
|
|
|
|
|
|
|
|
|
|
auto tab_layout = m_tab_bar->tab_layout();
|
2026-06-02 15:22:48 -03:00
|
|
|
auto side_margin = vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed);
|
2026-06-02 12:45:44 -03:00
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
m_vertical_tab_bar_column_layout->setContentsMargins(side_margin, VERTICAL_TABS_TOP_MARGIN, side_margin, 8);
|
|
|
|
|
update_vertical_tabs_button_layout();
|
2026-06-02 12:45:44 -03:00
|
|
|
|
|
|
|
|
update_tab_button_visibility();
|
|
|
|
|
update_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 15:56:24 -03:00
|
|
|
QRect TabWidget::vertical_tabs_chrome_rect() const
|
|
|
|
|
{
|
2026-06-03 05:19:56 -03:00
|
|
|
auto chrome_height = m_toolbar_container->height();
|
2026-06-02 15:56:24 -03:00
|
|
|
return {
|
|
|
|
|
0,
|
2026-06-03 05:19:56 -03:00
|
|
|
chrome_height,
|
2026-06-02 15:56:24 -03:00
|
|
|
current_vertical_tabs_width(),
|
2026-06-03 05:19:56 -03:00
|
|
|
max(0, height() - chrome_height)
|
2026-06-02 15:56:24 -03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 15:22:48 -03:00
|
|
|
int TabWidget::vertical_tabs_tab_width() const
|
|
|
|
|
{
|
|
|
|
|
return max(0, current_vertical_tabs_width() - vertical_tabs_horizontal_margin_width(m_tab_bar->tab_layout()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_vertical_tabs_button_layout()
|
2026-06-02 12:45:44 -03:00
|
|
|
{
|
2026-06-02 15:22:48 -03:00
|
|
|
auto expanded = vertical_tabs_effectively_expanded();
|
2026-06-02 15:34:12 -03:00
|
|
|
auto tool_button_style = expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly;
|
|
|
|
|
if (m_new_tab_button->toolButtonStyle() != tool_button_style)
|
|
|
|
|
m_new_tab_button->setToolButtonStyle(tool_button_style);
|
2026-06-02 15:22:48 -03:00
|
|
|
update_vertical_tabs_action_labels();
|
2026-06-02 15:34:12 -03:00
|
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_BUTTON_PROPERTY, true);
|
|
|
|
|
set_dynamic_property_if_needed(*m_new_tab_button, VERTICAL_TABS_EXPANDED_PROPERTY, expanded);
|
2026-06-02 15:22:48 -03:00
|
|
|
auto tab_width = vertical_tabs_tab_width();
|
2026-06-02 15:34:12 -03:00
|
|
|
auto button_size = QSize { tab_width, VERTICAL_TAB_HEIGHT };
|
|
|
|
|
if (m_new_tab_button->minimumSize() != button_size || m_new_tab_button->maximumSize() != button_size)
|
|
|
|
|
m_new_tab_button->setFixedSize(button_size);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
void TabWidget::update_tab_layout()
|
|
|
|
|
{
|
2026-05-31 14:17:47 -03:00
|
|
|
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
|
2026-06-01 10:46:39 -03:00
|
|
|
auto reserved_width = vertical_tabs_layout_width();
|
2026-05-31 19:37:00 -03:00
|
|
|
auto side_bar_width = current_vertical_tabs_width();
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tabs_reserved_space->setFixedWidth(reserved_width);
|
2026-05-31 14:17:47 -03:00
|
|
|
m_vertical_tab_bar_column->setFixedWidth(side_bar_width);
|
2026-06-02 15:22:48 -03:00
|
|
|
update_vertical_tabs_button_layout();
|
2026-06-01 10:46:39 -03:00
|
|
|
update_vertical_tabs_overlay_geometry();
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_content_layout->activate();
|
2026-06-02 15:22:48 -03:00
|
|
|
m_tab_bar->set_available_width(vertical_tabs_tab_width());
|
2026-05-31 19:37:00 -03:00
|
|
|
update_vertical_tabs_resize_handle();
|
2026-06-02 12:45:44 -03:00
|
|
|
update_vertical_tabs_content_separator();
|
2026-05-31 14:17:47 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:37:00 -03:00
|
|
|
update_vertical_tabs_resize_handle();
|
2026-06-02 12:45:44 -03:00
|
|
|
update_vertical_tabs_content_separator();
|
2026-05-31 19:37:00 -03:00
|
|
|
|
2026-05-27 13:18:10 -03:00
|
|
|
auto controls_width = m_new_tab_button->width();
|
2026-06-02 12:45:44 -03:00
|
|
|
if (m_window_controls->isVisible())
|
|
|
|
|
controls_width += m_window_controls->width();
|
2026-05-27 13:18:10 -03:00
|
|
|
|
2026-05-25 16:22:32 -03:00
|
|
|
auto available_for_tabs = width() - controls_width - 36;
|
2026-03-17 08:38:25 -03:00
|
|
|
|
|
|
|
|
m_tab_bar->set_available_width(available_for_tabs);
|
2026-06-01 08:41:54 -03:00
|
|
|
|
|
|
|
|
auto tab_bar_width = min(available_for_tabs, m_tab_bar->count() * HORIZONTAL_TAB_MAX_WIDTH);
|
|
|
|
|
m_tab_bar->setFixedWidth(max(0, tab_bar_width));
|
2026-03-17 08:38:25 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 14:17:47 -03:00
|
|
|
void TabWidget::update_tab_button_visibility()
|
|
|
|
|
{
|
2026-06-02 11:21:08 -03:00
|
|
|
if (m_tab_bar->tab_layout() == TabLayout::VerticalCollapsed) {
|
|
|
|
|
for (int index = 0; index < m_tab_bar->count(); ++index) {
|
|
|
|
|
if (auto* button = m_tab_bar->tabButton(index, QTabBar::RightSide))
|
|
|
|
|
button->hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_tab_bar->refresh_tab_layout();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-31 14:17:47 -03:00
|
|
|
|
|
|
|
|
for (int index = 0; index < m_tab_bar->count(); ++index) {
|
|
|
|
|
if (auto* button = m_tab_bar->tabButton(index, QTabBar::LeftSide))
|
2026-06-02 11:21:08 -03:00
|
|
|
button->setVisible(true);
|
2026-05-31 14:17:47 -03:00
|
|
|
if (auto* button = m_tab_bar->tabButton(index, QTabBar::RightSide))
|
2026-06-02 11:21:08 -03:00
|
|
|
button->setVisible(true);
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_tab_bar->refresh_tab_layout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_tab_chrome_visibility()
|
|
|
|
|
{
|
2026-06-02 12:45:44 -03:00
|
|
|
auto tab_layout = m_tab_bar->tab_layout();
|
|
|
|
|
auto is_horizontal = tab_layout == TabLayout::Horizontal;
|
|
|
|
|
auto show_top_row = m_tab_bar_visible && is_horizontal;
|
2026-05-31 14:17:47 -03:00
|
|
|
m_tab_bar_row->setVisible(show_top_row);
|
2026-06-03 13:27:18 -03:00
|
|
|
m_toolbar_container->setVisible(m_tab_bar_visible);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column->setVisible(m_tab_bar_visible && !is_horizontal);
|
|
|
|
|
update_vertical_tabs_content_separator();
|
|
|
|
|
|
|
|
|
|
auto show_window_controls = m_window_controls_visible
|
|
|
|
|
&& m_tab_bar_visible
|
2026-06-02 16:10:01 -03:00
|
|
|
&& is_horizontal;
|
2026-06-02 12:45:44 -03:00
|
|
|
m_window_controls->setVisible(show_window_controls);
|
2026-05-31 14:17:47 -03:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 08:38:25 -03:00
|
|
|
void TabWidget::recreate_icons()
|
|
|
|
|
{
|
2026-05-31 21:48:32 -03:00
|
|
|
m_tab_bar->recreate_icons();
|
2026-05-25 16:22:32 -03:00
|
|
|
m_new_tab_button->setIcon(create_chrome_icon(ChromeIcon::NewTab, palette()));
|
2026-05-31 19:37:00 -03:00
|
|
|
update_vertical_tabs_action_labels();
|
2026-05-25 16:21:50 -03:00
|
|
|
update_window_button_icons();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_chrome_style()
|
|
|
|
|
{
|
|
|
|
|
if (m_is_updating_chrome_style)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_is_updating_chrome_style = true;
|
2026-05-31 14:17:47 -03:00
|
|
|
auto style_sheet = ChromeStyle::tab_widget_style_sheet(palette());
|
|
|
|
|
m_tab_bar_row->setStyleSheet(style_sheet);
|
|
|
|
|
m_vertical_tab_bar_column->setStyleSheet(style_sheet);
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tabs_content_separator->setStyleSheet(style_sheet);
|
2026-05-31 19:37:00 -03:00
|
|
|
m_vertical_tabs_resize_handle->setStyleSheet(style_sheet);
|
2026-05-25 16:21:50 -03:00
|
|
|
m_is_updating_chrome_style = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 10:46:39 -03:00
|
|
|
void TabWidget::update_vertical_tabs_overlay_geometry()
|
|
|
|
|
{
|
2026-06-03 13:27:18 -03:00
|
|
|
if (!m_tab_bar_visible || m_tab_bar->tab_layout() == TabLayout::Horizontal) {
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column->hide();
|
2026-06-01 10:46:39 -03:00
|
|
|
return;
|
2026-06-02 12:45:44 -03:00
|
|
|
}
|
2026-06-01 10:46:39 -03:00
|
|
|
|
2026-06-02 15:56:24 -03:00
|
|
|
m_vertical_tab_bar_column->setGeometry(vertical_tabs_chrome_rect());
|
2026-06-02 12:45:44 -03:00
|
|
|
m_vertical_tab_bar_column->show();
|
2026-06-01 10:46:39 -03:00
|
|
|
m_vertical_tab_bar_column->raise();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::set_vertical_tabs_hover_expanded(bool expanded)
|
|
|
|
|
{
|
|
|
|
|
expanded &= can_expand_vertical_tabs_on_hover();
|
|
|
|
|
if (m_vertical_tabs_hover_expanded == expanded)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_vertical_tabs_hover_expanded = expanded;
|
|
|
|
|
if (m_vertical_tabs_hover_expanded)
|
|
|
|
|
m_vertical_tabs_hover_collapse_timer->start();
|
|
|
|
|
else
|
|
|
|
|
m_vertical_tabs_hover_collapse_timer->stop();
|
2026-06-02 12:45:44 -03:00
|
|
|
update_vertical_tabs_hover_layout();
|
2026-06-01 10:46:39 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::defer_update_vertical_tabs_hover_expanded()
|
|
|
|
|
{
|
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
|
|
|
update_vertical_tabs_hover_expanded();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::update_vertical_tabs_hover_expanded()
|
|
|
|
|
{
|
|
|
|
|
if (!m_vertical_tabs_hover_expanded)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (!cursor_is_over_vertical_tabs())
|
|
|
|
|
set_vertical_tabs_hover_expanded(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:21:50 -03:00
|
|
|
void TabWidget::update_window_button_icons()
|
|
|
|
|
{
|
2026-05-25 16:22:32 -03:00
|
|
|
auto is_maximized = window()->isMaximized();
|
|
|
|
|
m_minimize_window_button->setIcon(create_chrome_icon(ChromeIcon::WindowMinimize, palette()));
|
|
|
|
|
m_maximize_window_button->setIcon(create_chrome_icon(is_maximized ? ChromeIcon::WindowRestore : ChromeIcon::WindowMaximize, palette()));
|
|
|
|
|
m_maximize_window_button->setToolTip(is_maximized ? "Restore" : "Maximize");
|
|
|
|
|
m_close_window_button->setIcon(create_chrome_icon(ChromeIcon::WindowClose, palette()));
|
2026-05-31 19:35:03 -03:00
|
|
|
|
|
|
|
|
for (int index = 0; index < m_stacked_widget->count(); ++index)
|
|
|
|
|
tab(index)->update_window_control_icons();
|
2026-05-25 16:21:50 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TabWidget::toggle_window_maximized()
|
|
|
|
|
{
|
|
|
|
|
auto* top_level_window = window();
|
|
|
|
|
if (top_level_window->isMaximized())
|
|
|
|
|
top_level_window->showNormal();
|
|
|
|
|
else
|
|
|
|
|
top_level_window->showMaximized();
|
|
|
|
|
update_window_button_icons();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TabWidget::start_window_move()
|
|
|
|
|
{
|
|
|
|
|
auto* handle = window()->windowHandle();
|
|
|
|
|
if (!handle)
|
|
|
|
|
return false;
|
2026-06-02 12:45:44 -03:00
|
|
|
#if defined(AK_OS_MACOS)
|
|
|
|
|
if (start_appkit_window_drag(*this))
|
|
|
|
|
return true;
|
|
|
|
|
#endif
|
2026-05-25 16:21:50 -03:00
|
|
|
return handle->startSystemMove();
|
2024-07-26 10:39:26 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 11:21:08 -03:00
|
|
|
void TabBarButton::set_collapsed_vertical_overlay(bool enabled)
|
|
|
|
|
{
|
|
|
|
|
if (property(COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY).toBool() == enabled)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
setProperty(COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY, enabled);
|
|
|
|
|
|
|
|
|
|
QSize button_size { TAB_BUTTON_SIZE, TAB_BUTTON_SIZE };
|
|
|
|
|
QSize icon_size { TAB_ICON_SIZE, TAB_ICON_SIZE };
|
|
|
|
|
if (enabled) {
|
|
|
|
|
button_size = { COLLAPSED_VERTICAL_TAB_BUTTON_SIZE, COLLAPSED_VERTICAL_TAB_BUTTON_SIZE };
|
|
|
|
|
icon_size = { COLLAPSED_VERTICAL_TAB_ICON_SIZE, COLLAPSED_VERTICAL_TAB_ICON_SIZE };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setFixedSize(button_size);
|
|
|
|
|
setIconSize(icon_size);
|
|
|
|
|
style()->unpolish(this);
|
|
|
|
|
style()->polish(this);
|
|
|
|
|
update();
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-02 22:24:17 -03:00
|
|
|
TabBarButton::TabBarButton(QIcon const& icon, QWidget* parent)
|
|
|
|
|
: QPushButton(icon, {}, parent)
|
|
|
|
|
{
|
2026-05-25 16:21:50 -03:00
|
|
|
setObjectName("LadybirdTabButton");
|
2026-06-02 11:21:08 -03:00
|
|
|
setFixedSize({ TAB_BUTTON_SIZE, TAB_BUTTON_SIZE });
|
|
|
|
|
setIconSize({ TAB_ICON_SIZE, TAB_ICON_SIZE });
|
2026-05-25 16:22:32 -03:00
|
|
|
setFocusPolicy(Qt::NoFocus);
|
2024-04-02 22:24:17 -03:00
|
|
|
setFlat(true);
|
2026-06-02 11:21:08 -03:00
|
|
|
hide();
|
2024-04-02 22:24:17 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TabBarButton::event(QEvent* event)
|
|
|
|
|
{
|
|
|
|
|
if (event->type() == QEvent::Enter)
|
|
|
|
|
setFlat(false);
|
|
|
|
|
if (event->type() == QEvent::Leave)
|
|
|
|
|
setFlat(true);
|
|
|
|
|
|
|
|
|
|
return QPushButton::event(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|