UI/Qt: Polish vertical tab sidebar chrome

Refine vertical tab sidebar styling. Let inactive tabs sit on chrome.
Keep close buttons quiet. Align the New Tab row with tab rows.

Persist the expanded sidebar width and keep resizing bounded. Put the
resize hit area over the divider. Keep the collapsed rail fixed-width.
Avoid explicit mouse grabs while dragging.

Paint subtle sidebar dividers. Move the expand/collapse control into
the navigation toolbar. Tune the fallback globe and vertical-tabs icons.
This commit is contained in:
Andreas Kling 2026-06-01 00:37:00 +02:00 committed by Andreas Kling
parent a7e68ef1ae
commit 24e767a06e
11 changed files with 365 additions and 95 deletions

View file

@ -840,7 +840,7 @@ void BrowserWindow::initialize_tab_buttons(Tab* tab)
auto index = m_tabs_container->index_of(tab);
m_tabs_container->set_tab_icon(index, tab->tab_icon());
auto* close_button = new TabBarButton(create_chrome_icon(ChromeIcon::Close, palette()));
auto* close_button = new TabBarButton(create_chrome_icon(ChromeIcon::TabClose, palette()));
close_button->setToolTip("Close Tab");
connect(close_button, &QPushButton::clicked, this, [this, tab]() {
@ -861,7 +861,7 @@ void BrowserWindow::update_tab_close_button_icons()
return;
if (auto* tab_bar_button = qobject_cast<TabBarButton*>(button))
tab_bar_button->setIcon(create_chrome_icon(ChromeIcon::Close, palette()));
tab_bar_button->setIcon(create_chrome_icon(ChromeIcon::TabClose, palette()));
}
}

View file

@ -179,7 +179,7 @@ static QColor chrome_control_surface_pressed(QPalette const& palette)
return mix(chrome_surface(palette), material_color_anchors(dark).pressed, dark ? 0.86 : 0.66);
}
static QColor chrome_control_border(QPalette const& palette)
QColor chrome_control_border(QPalette const& palette)
{
auto dark = is_dark(palette);
if (dark)
@ -601,7 +601,8 @@ QToolButton#LadybirdLocationZoomIndicator:pressed {{
QToolButton#LadybirdLocationAction {{
background: transparent;
border: 0;
border-radius: 12px;
border-radius: 11px;
margin: 1px;
padding: 0;
}}
@ -750,6 +751,7 @@ QString tab_widget_style_sheet(QPalette const& palette)
auto tab_strip_bottom = chrome_tab_strip_background_bottom(palette);
auto background = style_sheet_color(chrome_tab_strip_background(palette));
auto background_bottom = style_sheet_color(tab_strip_bottom);
auto sidebar_background = style_sheet_color(dark ? tab_strip_bottom : mix(tab_strip_bottom, chrome_active_tab_surface_bottom(palette), 0.34));
auto hover = style_sheet_color(chrome_control_surface_hover(palette));
auto pressed = style_sheet_color(chrome_control_surface_pressed(palette));
auto control_border = style_sheet_color(chrome_control_border(palette));
@ -757,7 +759,8 @@ QString tab_widget_style_sheet(QPalette const& palette)
auto close_hover = style_sheet_color(chrome_destructive_hover());
auto close_text = style_sheet_color(chrome_destructive_text());
auto strip_separator = dark ? background_bottom : control_border;
auto sidebar_separator = style_sheet_color(mix(tab_strip_bottom, chrome_border(palette), dark ? 0.44 : 0.58));
auto sidebar_separator_hover = style_sheet_color(mix(tab_strip_bottom, chrome_border(palette), dark ? 0.64 : 0.76));
return qformatted(R"(
QWidget#LadybirdTabStrip {{
color: {5};
@ -768,12 +771,27 @@ QWidget#LadybirdTabStrip {{
QWidget#LadybirdVerticalTabBar {{
color: {5};
background: {1};
border-right: 1px solid {8};
background: {11};
border-right: 1px solid {9};
}}
QWidget#LadybirdVerticalTabBar[hovered="true"],
QWidget#LadybirdVerticalTabBar[active="true"] {{
border-right: 1px solid {10};
}}
QWidget#LadybirdVerticalTabsResizeHandle {{
background: transparent;
border: 0;
}}
QWidget#LadybirdVerticalTabsSeparator {{
background: {9};
min-height: 1px;
max-height: 1px;
}}
QToolButton#LadybirdNewTabButton,
QToolButton#LadybirdVerticalTabsToggleButton,
QPushButton#LadybirdTabButton {{
color: {5};
background: transparent;
@ -782,19 +800,12 @@ QPushButton#LadybirdTabButton {{
padding: 0;
}}
QToolButton#LadybirdNewTabButton,
QToolButton#LadybirdVerticalTabsToggleButton {{
QToolButton#LadybirdNewTabButton {{
min-width: 30px;
min-height: 30px;
border-radius: 16px;
}}
QToolButton#LadybirdNewTabButton[verticalTabsExpanded="true"],
QToolButton#LadybirdVerticalTabsToggleButton[verticalTabsExpanded="true"] {{
padding-left: 14px;
text-align: left;
}}
QPushButton#LadybirdTabButton {{
min-width: 22px;
min-height: 22px;
@ -803,16 +814,16 @@ QPushButton#LadybirdTabButton {{
}}
QToolButton#LadybirdNewTabButton:hover,
QToolButton#LadybirdVerticalTabsToggleButton:hover,
QPushButton#LadybirdTabButton:hover {{
color: {5};
background: {2};
border-color: {4};
}}
QToolButton#LadybirdNewTabButton:pressed,
QToolButton#LadybirdVerticalTabsToggleButton:pressed,
QPushButton#LadybirdTabButton:pressed,
QPushButton#LadybirdTabButton:checked {{
color: {5};
background: {3};
border-color: {4};
}}
@ -852,7 +863,8 @@ QToolButton#LadybirdCloseWindowButton[pressedOutside="true"] {{
background: transparent;
}}
)",
background, background_bottom, hover, pressed, control_border, text, close_hover, close_text, strip_separator);
background, background_bottom, hover, pressed, control_border, text, close_hover, close_text, strip_separator,
sidebar_separator, sidebar_separator_hover, sidebar_background);
}
QString autocomplete_popup_style_sheet(QPalette const& palette)

View file

@ -21,6 +21,7 @@ QColor chrome_surface(QPalette const&);
QColor chrome_surface_recessed(QPalette const&);
QColor chrome_surface_hover(QPalette const&);
QColor chrome_surface_pressed(QPalette const&);
QColor chrome_control_border(QPalette const&);
QColor chrome_active_tab_surface_top(QPalette const&);
QColor chrome_active_tab_surface_bottom(QPalette const&);
QColor chrome_border(QPalette const&);

View file

@ -113,34 +113,37 @@ static void draw_vertical_tab_bar_icon(QPainter& painter, QColor const& color, b
{
painter.setPen(chrome_icon_pen(color, 1.55));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(QRectF(3.8, 4.2, 12.4, 11.6), 1.6, 1.6);
painter.drawLine(QPointF(7.8, 4.6), QPointF(7.8, 15.4));
painter.drawRoundedRect(QRectF(3.8, 3.2, 12.4, 11.6), 1.6, 1.6);
painter.drawLine(QPointF(7.8, 3.6), QPointF(7.8, 14.4));
QPainterPath arrow;
if (expanded) {
arrow.moveTo(12.6, 7.2);
arrow.lineTo(9.8, 10.0);
arrow.lineTo(12.6, 12.8);
arrow.moveTo(13.1, 6.8);
arrow.lineTo(10.3, 9.0);
arrow.lineTo(13.1, 11.2);
} else {
arrow.moveTo(10.2, 7.2);
arrow.lineTo(13.0, 10.0);
arrow.lineTo(10.2, 12.8);
arrow.moveTo(10.7, 6.8);
arrow.lineTo(13.5, 9.0);
arrow.lineTo(10.7, 11.2);
}
draw_stroked_icon_path(painter, arrow, color, 1.85);
}
static void draw_globe_icon(QPainter& painter, QColor const& color)
{
painter.setPen(chrome_icon_pen(color, 1.55));
painter.setBrush(Qt::NoBrush);
auto outline_rect = QRectF(3.5, 3.5, 13, 13);
painter.drawEllipse(outline_rect);
painter.drawLine(QPointF(outline_rect.left() + 1.3, 10), QPointF(outline_rect.right() - 1.3, 10));
QRectF meridian_rect(7.5, outline_rect.top() + 0.25, 5.0, outline_rect.height() - 0.5);
QRectF meridian_rect(6.5, outline_rect.top(), 7.0, outline_rect.height());
painter.drawArc(meridian_rect, 90 * 16, 180 * 16);
painter.drawArc(meridian_rect, 270 * 16, 180 * 16);
QPainterPath path;
path.addEllipse(outline_rect);
path.moveTo(QPointF(outline_rect.left() + 1.3, 10));
path.lineTo(QPointF(outline_rect.right() - 1.3, 10));
path.arcMoveTo(meridian_rect, 90);
path.arcTo(meridian_rect, 90, 180);
path.arcMoveTo(meridian_rect, 270);
path.arcTo(meridian_rect, 270, 180);
draw_stroked_icon_path(painter, path, color, 1.55);
}
static QPixmap create_transparent_icon_pixmap(QSize logical_size, qreal device_pixel_ratio)
@ -184,6 +187,11 @@ static QPixmap create_chrome_icon_pixmap(ChromeIcon icon, QColor color, qreal de
painter.drawLine(QPointF(5.5, 7.15), QPointF(14.5, 16.65));
painter.drawLine(QPointF(14.5, 7.15), QPointF(5.5, 16.65));
break;
case ChromeIcon::TabClose:
painter.setPen(chrome_icon_pen(color, 1.9));
painter.drawLine(QPointF(5.5, 5.65), QPointF(14.5, 15.15));
painter.drawLine(QPointF(14.5, 5.65), QPointF(5.5, 15.15));
break;
case ChromeIcon::Menu:
painter.setPen(chrome_icon_pen(color, 1.75));
painter.drawLine(QPointF(4.1, 6.2), QPointF(15.9, 6.2));
@ -290,7 +298,7 @@ QIcon create_chrome_icon(ChromeIcon icon, QPalette const& palette)
{
auto normal = ChromeStyle::chrome_button_text(palette);
auto normal_alpha = 216;
if (icon == ChromeIcon::Close)
if (icon == ChromeIcon::Close || icon == ChromeIcon::TabClose)
normal_alpha = 172;
else if (icon == ChromeIcon::Star)
normal_alpha = 204;
@ -299,10 +307,10 @@ QIcon create_chrome_icon(ChromeIcon icon, QPalette const& palette)
normal.setAlpha(normal_alpha);
auto active = ChromeStyle::chrome_button_text(palette);
active.setAlpha(icon == ChromeIcon::Close ? 220 : 236);
active.setAlpha(icon == ChromeIcon::Close || icon == ChromeIcon::TabClose ? 220 : 236);
auto disabled = ChromeStyle::chrome_muted_text(palette);
disabled.setAlpha(icon == ChromeIcon::Close ? 78 : 96);
disabled.setAlpha(icon == ChromeIcon::Close || icon == ChromeIcon::TabClose ? 78 : 96);
QIcon qicon;

View file

@ -23,6 +23,7 @@ enum class ChromeIcon {
Stop,
NewTab,
Close,
TabClose,
Menu,
Star,
StarFilled,

View file

@ -65,4 +65,16 @@ void Settings::set_show_menubar(bool show_menubar)
emit show_menubar_changed(show_menubar);
}
Optional<int> Settings::vertical_tabs_expanded_width()
{
if (m_qsettings->contains("vertical_tabs_expanded_width"))
return m_qsettings->value("vertical_tabs_expanded_width").toInt();
return {};
}
void Settings::set_vertical_tabs_expanded_width(int width)
{
m_qsettings->setValue("vertical_tabs_expanded_width", width);
}
}

View file

@ -9,6 +9,7 @@
#pragma once
#include <AK/ByteString.h>
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <QPoint>
@ -44,6 +45,9 @@ public:
bool show_menubar();
void set_show_menubar(bool show_menubar);
Optional<int> vertical_tabs_expanded_width();
void set_vertical_tabs_expanded_width(int);
signals:
void show_menubar_changed(bool show_menubar);

View file

@ -88,6 +88,10 @@ static QToolButton* create_toolbar_button(QWidget& parent, QAction& action)
return button;
}
static constexpr int TOOLBAR_HORIZONTAL_MARGIN = 12;
static constexpr int TOOLBAR_VERTICAL_MARGIN = 2;
static constexpr int TOOLBAR_WINDOW_CONTROLS_RIGHT_MARGIN = 4;
Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
: QWidget(window)
, m_window(window)
@ -122,7 +126,7 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
auto* toolbar_layout = new QHBoxLayout(m_toolbar);
toolbar_layout->setSpacing(6);
toolbar_layout->setContentsMargins(12, 2, 12, 2);
toolbar_layout->setContentsMargins(TOOLBAR_HORIZONTAL_MARGIN, TOOLBAR_VERTICAL_MARGIN, TOOLBAR_HORIZONTAL_MARGIN, TOOLBAR_VERTICAL_MARGIN);
m_location_edit = new LocationEdit(this);
m_bookmarks_bar = new BookmarksBar(this);
@ -167,6 +171,7 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
m_navigate_back_action = create_application_action(*this, view().navigate_back_action());
m_navigate_forward_action = create_application_action(*this, view().navigate_forward_action());
m_reload_action = create_application_action(*this, application.reload_action());
m_toggle_vertical_tabs_expanded_action = create_application_action(*this, application.toggle_vertical_tabs_expanded_action());
m_toolbar_window_controls_separator = new QWidget(m_toolbar);
m_toolbar_window_controls_separator->setObjectName("LadybirdToolbarWindowControlsSeparator");
@ -210,6 +215,9 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
auto* navigation_button_layout = new QHBoxLayout(navigation_button_cluster);
navigation_button_layout->setSpacing(2);
navigation_button_layout->setContentsMargins(0, 0, 0, 0);
m_toggle_vertical_tabs_expanded_button = create_toolbar_button(*navigation_button_cluster, *m_toggle_vertical_tabs_expanded_action);
m_toggle_vertical_tabs_expanded_button->setVisible(WebView::Application::settings().tab_settings().vertical_tabs_enabled);
navigation_button_layout->addWidget(m_toggle_vertical_tabs_expanded_button);
navigation_button_layout->addWidget(create_toolbar_button(*navigation_button_cluster, *m_navigate_back_action));
navigation_button_layout->addWidget(create_toolbar_button(*navigation_button_cluster, *m_navigate_forward_action));
navigation_button_layout->addWidget(create_toolbar_button(*navigation_button_cluster, *m_reload_action));
@ -224,7 +232,8 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
update_chrome_style();
set_toolbar_window_controls_visible(!Settings::the()->show_menubar() && WebView::Application::settings().tab_settings().vertical_tabs_enabled);
set_toolbar_window_drag_enabled(WebView::Application::settings().tab_settings().vertical_tabs_enabled);
set_vertical_tabs_enabled(WebView::Application::settings().tab_settings().vertical_tabs_enabled);
set_vertical_tabs_expanded(WebView::Application::settings().tab_settings().vertical_tabs_expanded);
m_hamburger_button->setVisible(!Settings::the()->show_menubar());
@ -571,10 +580,23 @@ void Tab::set_window(BrowserWindow& window)
recreate_toolbar_icons();
}
void Tab::set_vertical_tabs_enabled(bool enabled)
{
m_toggle_vertical_tabs_expanded_button->setVisible(enabled);
recreate_toolbar_icons();
set_toolbar_window_drag_enabled(enabled);
}
void Tab::set_vertical_tabs_expanded(bool)
{
recreate_toolbar_icons();
}
void Tab::set_toolbar_window_controls_visible(bool visible)
{
m_toolbar_window_controls_separator->setVisible(visible);
m_toolbar_window_controls->setVisible(visible);
m_toolbar->layout()->setContentsMargins(TOOLBAR_HORIZONTAL_MARGIN, TOOLBAR_VERTICAL_MARGIN, visible ? TOOLBAR_WINDOW_CONTROLS_RIGHT_MARGIN : TOOLBAR_HORIZONTAL_MARGIN, TOOLBAR_VERTICAL_MARGIN);
}
void Tab::set_toolbar_window_drag_enabled(bool enabled)
@ -744,6 +766,11 @@ void Tab::recreate_toolbar_icons()
m_navigate_back_action->setIcon(create_chrome_icon(ChromeIcon::Back, palette()));
m_navigate_forward_action->setIcon(create_chrome_icon(ChromeIcon::Forward, palette()));
m_reload_action->setIcon(create_chrome_icon(ChromeIcon::Reload, palette()));
m_toggle_vertical_tabs_expanded_action->setIcon(create_chrome_icon(
WebView::Application::settings().tab_settings().vertical_tabs_expanded
? ChromeIcon::VerticalTabBarCollapse
: ChromeIcon::VerticalTabBarExpand,
palette()));
m_hamburger_button->setIcon(create_chrome_icon(ChromeIcon::Menu, palette()));
update_window_control_icons();
if (auto* action = m_location_edit->trailing_action()) {

View file

@ -80,6 +80,8 @@ public:
QToolButton* hamburger_button() const { return m_hamburger_button; }
void set_vertical_tabs_enabled(bool);
void set_vertical_tabs_expanded(bool);
void set_window(BrowserWindow&);
void set_toolbar_window_controls_visible(bool);
void set_toolbar_window_drag_enabled(bool);
@ -119,6 +121,7 @@ private:
WindowControlButton* m_maximize_window_button { nullptr };
WindowControlButton* m_close_window_button { nullptr };
BookmarksBar* m_bookmarks_bar { nullptr };
QToolButton* m_toggle_vertical_tabs_expanded_button { nullptr };
QToolButton* m_hamburger_button { nullptr };
LocationEdit* m_location_edit { nullptr };
WebContentView* m_view { nullptr };
@ -142,6 +145,7 @@ private:
QAction* m_navigate_back_action { nullptr };
QAction* m_navigate_forward_action { nullptr };
QAction* m_reload_action { nullptr };
QAction* m_toggle_vertical_tabs_expanded_action { nullptr };
QPointer<QDialog> m_dialog;

View file

@ -12,6 +12,7 @@
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/Tab.h>
#include <UI/Qt/TabBar.h>
#include <UI/Qt/WindowControlButton.h>
@ -50,10 +51,15 @@ static constexpr int HORIZONTAL_TAB_STRIP_HEIGHT = 43;
static constexpr int HORIZONTAL_TAB_HEIGHT = 36;
static constexpr int VERTICAL_TAB_HEIGHT = 38;
static constexpr int VERTICAL_TABS_COLLAPSED_WIDTH = 52;
static constexpr int VERTICAL_TABS_EXPANDED_WIDTH = 248;
static constexpr int VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH = 232;
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;
static constexpr int VERTICAL_TABS_SIDE_MARGIN = 6;
static constexpr int TAB_ICON_SIZE = 16;
static constexpr auto VERTICAL_TABS_EXPANDED_PROPERTY = "verticalTabsExpanded";
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY = "hovered";
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY = "active";
static QPointer<TabWidget> s_active_tab_drag_source;
static QPointer<Tab> s_active_tab_dragged_tab;
@ -89,9 +95,73 @@ static constexpr int vertical_tab_width(int available_width, TabLayout tab_layou
{
if (available_width > 0)
return available_width;
return (tab_layout == TabLayout::VerticalCollapsed ? VERTICAL_TABS_COLLAPSED_WIDTH : VERTICAL_TABS_EXPANDED_WIDTH) - (VERTICAL_TABS_SIDE_MARGIN * 2);
auto width = tab_layout == TabLayout::VerticalCollapsed ? VERTICAL_TABS_COLLAPSED_WIDTH : VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH;
return width - (VERTICAL_TABS_SIDE_MARGIN * 2);
}
static constexpr int clamp_vertical_tabs_expanded_width(int width)
{
return clamp(width, VERTICAL_TABS_MIN_EXPANDED_WIDTH, VERTICAL_TABS_MAX_EXPANDED_WIDTH);
}
class NewTabButton final : public QToolButton {
public:
using QToolButton::QToolButton;
private:
virtual void paintEvent(QPaintEvent* event) override
{
if (!property(VERTICAL_TABS_EXPANDED_PROPERTY).toBool()) {
QToolButton::paintEvent(event);
return;
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
auto dark = ChromeStyle::is_dark(palette());
auto surface = ChromeStyle::chrome_surface(palette());
auto shape_rect = QRectF(rect()).adjusted(7.0, 3.0, -7.0, -3.0);
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
if (isDown()) {
painter.setBrush(ChromeStyle::chrome_surface_pressed(palette()));
painter.setPen(QPen(ChromeStyle::chrome_control_border(palette()), 1));
painter.drawPath(tab_path);
} else if (underMouse()) {
auto hover = dark ? ChromeStyle::chrome_surface_hover(palette()) : ChromeStyle::mix(surface, ChromeStyle::chrome_surface_hover(palette()), 0.28);
hover.setAlpha(dark ? 120 : 136);
painter.setBrush(hover);
painter.setPen(QPen(ChromeStyle::chrome_control_border(palette()), 1));
painter.drawPath(tab_path);
}
auto contents_rect = shape_rect.toAlignedRect().adjusted(12, 0, -12, 0);
QRect icon_rect {
contents_rect.left(),
contents_rect.top() + ((contents_rect.height() - TAB_ICON_SIZE) / 2),
TAB_ICON_SIZE,
TAB_ICON_SIZE,
};
if (!underMouse() && !isDown())
painter.setOpacity(dark ? 0.90 : 0.84);
icon().paint(&painter, icon_rect);
painter.setOpacity(1.0);
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);
if (underMouse() || isDown())
text_color = ChromeStyle::chrome_button_text(palette());
painter.setPen(text_color);
painter.setFont(font());
QFontMetrics font_metrics(font());
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);
}
};
TabBar::TabBar(TabWidget* tab_widget)
: QTabBar(tab_widget)
, m_tab_widget(tab_widget)
@ -264,6 +334,9 @@ void TabBar::paintEvent(QPaintEvent*)
painter.setBrush(hover);
painter.setPen(QPen(hover_border, 1));
painter.drawPath(tab_path);
} else if (is_vertical) {
// Let background vertical tabs sit directly on the sidebar so they
// do not read as disabled cards.
} else {
auto inactive = ChromeStyle::chrome_surface_recessed(palette());
inactive.setAlpha(dark ? 104 : 150);
@ -274,7 +347,7 @@ void TabBar::paintEvent(QPaintEvent*)
painter.drawPath(tab_path);
}
if (!is_selected && !is_hovered && index > 0 && index != currentIndex() + 1) {
if (!is_selected && !is_hovered && index > 0 && index != currentIndex() + 1 && !is_collapsed_vertical) {
auto separator = border;
separator.setAlpha(dark ? 42 : 36);
painter.setPen(separator);
@ -290,8 +363,8 @@ void TabBar::paintEvent(QPaintEvent*)
if (is_collapsed_vertical) {
QRect icon_rect {
tab_rect.center().x() - (TAB_ICON_SIZE / 2),
tab_rect.center().y() - (TAB_ICON_SIZE / 2),
tab_rect.center().x() - (TAB_ICON_SIZE / 2) + 1,
tab_rect.center().y() - (TAB_ICON_SIZE / 2) + 1,
TAB_ICON_SIZE,
TAB_ICON_SIZE,
};
@ -313,7 +386,7 @@ void TabBar::paintEvent(QPaintEvent*)
TAB_ICON_SIZE,
};
icon.paint(&painter, icon_rect);
contents_rect.setLeft(icon_rect.right() + 7);
contents_rect.setLeft(icon_rect.right() + (is_vertical ? 8 : 7));
}
QFont tab_font = font();
@ -425,8 +498,12 @@ void TabBar::mousePressEvent(QMouseEvent* event)
}
if (tab_layout() != TabLayout::Horizontal) {
if (pressed_tab >= 0)
if (pressed_tab >= 0) {
setCurrentIndex(pressed_tab);
} else if (event->button() == Qt::LeftButton && start_window_move()) {
event->accept();
return;
}
} else {
QTabBar::mousePressEvent(event);
}
@ -730,7 +807,7 @@ void TabBar::update_tab_button_geometry()
auto* button = tabButton(index, position);
if (!button)
return;
button->setVisible(true);
button->setVisible(position != QTabBar::RightSide || index == currentIndex() || index == m_hovered_tab_index);
auto button_size = button->size();
if (button_size.isEmpty())
@ -764,9 +841,20 @@ void TabBar::toggle_window_maximized()
top_level_window->showMaximized();
}
bool TabBar::start_window_move()
{
auto* handle = window()->windowHandle();
if (!handle)
return false;
return handle->startSystemMove();
}
TabWidget::TabWidget(QWidget* parent)
: QWidget(parent)
{
m_vertical_tabs_expanded_width = Settings::the()->vertical_tabs_expanded_width().value_or(VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH);
m_vertical_tabs_expanded_width = clamp_vertical_tabs_expanded_width(m_vertical_tabs_expanded_width);
m_tab_bar = new TabBar(this);
setAcceptDrops(true);
@ -780,25 +868,22 @@ TabWidget::TabWidget(QWidget* parent)
m_stacked_widget = new QStackedWidget(this);
m_new_tab_button = new QToolButton(this);
m_new_tab_button = new NewTabButton(this);
m_new_tab_button->setObjectName("LadybirdNewTabButton");
m_new_tab_button->setIconSize(QSize(18, 18));
m_new_tab_button->setFixedSize(32, 32);
m_new_tab_button->setFocusPolicy(Qt::NoFocus);
m_new_tab_button->setToolTip("New Tab");
m_toggle_vertical_tabs_expanded_action = create_application_action(*this, WebView::Application::the().toggle_vertical_tabs_expanded_action());
m_toggle_vertical_tabs_expanded_button = new QToolButton(this);
m_toggle_vertical_tabs_expanded_button->setObjectName("LadybirdVerticalTabsToggleButton");
m_toggle_vertical_tabs_expanded_button->setDefaultAction(m_toggle_vertical_tabs_expanded_action);
m_toggle_vertical_tabs_expanded_button->setIconSize(QSize(18, 18));
m_toggle_vertical_tabs_expanded_button->setFixedSize(32, 32);
m_toggle_vertical_tabs_expanded_button->setFocusPolicy(Qt::NoFocus);
m_minimize_window_button = new WindowControlButton("LadybirdWindowButton", "Minimize", { 18, 18 }, { 40, 40 }, this);
m_maximize_window_button = new WindowControlButton("LadybirdWindowButton", "Maximize", { 18, 18 }, { 40, 40 }, this);
m_close_window_button = new WindowControlButton("LadybirdCloseWindowButton", "Close", { 18, 18 }, { 40, 40 }, this);
m_vertical_tabs_new_tab_separator = new QWidget(this);
m_vertical_tabs_new_tab_separator->setObjectName("LadybirdVerticalTabsSeparator");
m_vertical_tabs_new_tab_separator->setFixedHeight(1);
m_vertical_tabs_new_tab_separator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
recreate_icons();
m_tab_bar_row = new QWidget(this);
@ -809,12 +894,21 @@ TabWidget::TabWidget(QWidget* parent)
m_vertical_tab_bar_column = new QWidget(this);
m_vertical_tab_bar_column->setObjectName("LadybirdVerticalTabBar");
m_vertical_tab_bar_column_layout = new QVBoxLayout(m_vertical_tab_bar_column);
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);
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_resize_handle = new QWidget(m_vertical_tabs_content);
m_vertical_tabs_resize_handle->setObjectName("LadybirdVerticalTabsResizeHandle");
m_vertical_tabs_resize_handle->setCursor(Qt::SizeHorCursor);
m_vertical_tabs_resize_handle->installEventFilter(this);
m_vertical_tabs_resize_handle->hide();
m_main_layout = new QVBoxLayout(this);
m_main_layout->setSpacing(0);
m_main_layout->setContentsMargins(0, 0, 0, 0);
@ -862,7 +956,8 @@ void TabWidget::insert_tab(int index, Tab* widget, QString const& label)
m_tab_bar->insertTab(index, label);
widget->set_toolbar_window_controls_visible(m_window_controls_visible && m_vertical_tabs_enabled);
widget->set_toolbar_window_drag_enabled(m_vertical_tabs_enabled);
widget->set_vertical_tabs_enabled(m_vertical_tabs_enabled);
widget->set_vertical_tabs_expanded(m_vertical_tabs_expanded);
update_tab_layout();
update_tab_button_visibility();
@ -942,7 +1037,7 @@ void TabWidget::set_vertical_tabs_enabled(bool enabled)
m_vertical_tabs_enabled = enabled;
for (int index = 0; index < m_stacked_widget->count(); ++index) {
tab(index)->set_toolbar_window_controls_visible(m_window_controls_visible && enabled);
tab(index)->set_toolbar_window_drag_enabled(enabled);
tab(index)->set_vertical_tabs_enabled(enabled);
}
rebuild_layout();
}
@ -953,6 +1048,8 @@ void TabWidget::set_vertical_tabs_expanded(bool expanded)
return;
m_vertical_tabs_expanded = expanded;
for (int index = 0; index < m_stacked_widget->count(); ++index)
tab(index)->set_vertical_tabs_expanded(expanded);
rebuild_layout();
}
@ -985,14 +1082,70 @@ void TabWidget::dropEvent(QDropEvent* event)
bool TabWidget::eventFilter(QObject* watched, QEvent* event)
{
if (watched == m_tab_bar_row) {
auto is_empty_tab_strip_area = [this](QMouseEvent const& mouse_event) {
if (watched == m_vertical_tabs_resize_handle) {
auto reset_resize_handle = [this] {
m_is_resizing_vertical_tabs = false;
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;
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;
}
}
if (watched == m_tab_bar_row || watched == m_vertical_tab_bar_column) {
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());
return child == nullptr || child == m_vertical_tabs_new_tab_separator;
}
return m_tab_bar_row->childAt(mouse_event.pos()) == nullptr;
};
if (event->type() == QEvent::MouseButtonDblClick) {
auto* mouse_event = static_cast<QMouseEvent*>(event);
if (mouse_event->button() == Qt::LeftButton && is_empty_tab_strip_area(*mouse_event)) {
if (mouse_event->button() == Qt::LeftButton && is_empty_chrome_area(*mouse_event)) {
toggle_window_maximized();
return true;
}
@ -1000,7 +1153,7 @@ bool TabWidget::eventFilter(QObject* watched, QEvent* event)
if (event->type() == QEvent::MouseButtonPress) {
auto* mouse_event = static_cast<QMouseEvent*>(event);
if (mouse_event->button() == Qt::LeftButton && is_empty_tab_strip_area(*mouse_event) && start_window_move())
if (mouse_event->button() == Qt::LeftButton && is_empty_chrome_area(*mouse_event) && start_window_move())
return true;
}
}
@ -1123,63 +1276,103 @@ void TabWidget::rebuild_layout_for_vertical_tabs()
clear_layout(*m_tab_bar_row_layout);
clear_layout(*m_vertical_tab_bar_column_layout);
auto side_bar_width = m_vertical_tabs_expanded ? VERTICAL_TABS_EXPANDED_WIDTH : VERTICAL_TABS_COLLAPSED_WIDTH;
auto side_bar_width = current_vertical_tabs_width();
m_vertical_tab_bar_column->setFixedWidth(side_bar_width);
m_vertical_tab_bar_column_layout->setSpacing(4);
m_vertical_tab_bar_column_layout->setSpacing(m_vertical_tabs_expanded ? 0 : 4);
m_vertical_tab_bar_column_layout->setContentsMargins(VERTICAL_TABS_SIDE_MARGIN, 8, VERTICAL_TABS_SIDE_MARGIN, 8);
m_new_tab_button->setToolButtonStyle(m_vertical_tabs_expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly);
m_new_tab_button->setText(m_vertical_tabs_expanded ? "New Tab" : QString {});
update_vertical_tabs_action_labels();
m_new_tab_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, m_vertical_tabs_expanded);
m_new_tab_button->style()->unpolish(m_new_tab_button);
m_new_tab_button->style()->polish(m_new_tab_button);
m_toggle_vertical_tabs_expanded_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, m_vertical_tabs_expanded);
m_toggle_vertical_tabs_expanded_button->style()->unpolish(m_toggle_vertical_tabs_expanded_button);
m_toggle_vertical_tabs_expanded_button->style()->polish(m_toggle_vertical_tabs_expanded_button);
m_vertical_tabs_new_tab_separator->setVisible(!m_vertical_tabs_expanded);
if (m_vertical_tabs_expanded) {
m_new_tab_button->setFixedHeight(32);
m_new_tab_button->setFixedHeight(VERTICAL_TAB_HEIGHT);
m_new_tab_button->setMinimumWidth(32);
m_new_tab_button->setMaximumWidth(QWIDGETSIZE_MAX);
m_new_tab_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
m_toggle_vertical_tabs_expanded_button->setText("Collapse Tabs");
m_toggle_vertical_tabs_expanded_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_toggle_vertical_tabs_expanded_button->setFixedHeight(32);
m_toggle_vertical_tabs_expanded_button->setMinimumWidth(32);
m_toggle_vertical_tabs_expanded_button->setMaximumWidth(QWIDGETSIZE_MAX);
m_toggle_vertical_tabs_expanded_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
} else {
m_new_tab_button->setFixedSize(32, 32);
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
m_toggle_vertical_tabs_expanded_button->setText({});
m_toggle_vertical_tabs_expanded_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_toggle_vertical_tabs_expanded_button->setFixedSize(32, 32);
m_toggle_vertical_tabs_expanded_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
Qt::Alignment new_tab_button_alignment {};
Qt::Alignment toggle_button_alignment {};
if (!m_vertical_tabs_expanded) {
if (!m_vertical_tabs_expanded)
new_tab_button_alignment = Qt::AlignHCenter;
toggle_button_alignment = Qt::AlignHCenter;
}
m_vertical_tab_bar_column_layout->addWidget(m_tab_bar);
m_vertical_tab_bar_column_layout->addWidget(m_vertical_tabs_new_tab_separator);
m_vertical_tab_bar_column_layout->addWidget(m_new_tab_button, 0, new_tab_button_alignment);
m_vertical_tab_bar_column_layout->addStretch(1);
m_vertical_tab_bar_column_layout->addWidget(m_toggle_vertical_tabs_expanded_button, 0, toggle_button_alignment);
}
int TabWidget::current_vertical_tabs_width() const
{
return m_vertical_tabs_expanded ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH;
}
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()
{
Settings::the()->set_vertical_tabs_expanded_width(m_vertical_tabs_expanded_width);
}
void TabWidget::set_resize_handle_property(char const* property, bool enabled)
{
if (m_vertical_tab_bar_column->property(property).toBool() == enabled)
return;
m_vertical_tab_bar_column->setProperty(property, enabled);
m_vertical_tab_bar_column->style()->unpolish(m_vertical_tab_bar_column);
m_vertical_tab_bar_column->style()->polish(m_vertical_tab_bar_column);
m_vertical_tab_bar_column->update();
}
void TabWidget::update_vertical_tabs_resize_handle()
{
auto is_vertical = m_tab_bar->tab_layout() != TabLayout::Horizontal;
auto show_resize_handle = is_vertical && m_vertical_tabs_expanded;
m_vertical_tabs_resize_handle->setVisible(show_resize_handle);
if (!show_resize_handle) {
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;
auto divider_x = m_vertical_tab_bar_column->geometry().right();
m_vertical_tabs_resize_handle->setGeometry(divider_x - (handle_width / 2), 0, handle_width, m_vertical_tabs_content->height());
m_vertical_tabs_resize_handle->raise();
}
void TabWidget::update_vertical_tabs_action_labels()
{
m_new_tab_button->setText(m_vertical_tabs_expanded ? "New Tab" : QString {});
}
void TabWidget::update_tab_layout()
{
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
auto side_bar_width = m_vertical_tabs_expanded ? VERTICAL_TABS_EXPANDED_WIDTH : VERTICAL_TABS_COLLAPSED_WIDTH;
auto side_bar_width = current_vertical_tabs_width();
m_vertical_tab_bar_column->setFixedWidth(side_bar_width);
m_tab_bar->set_available_width(side_bar_width - (VERTICAL_TABS_SIDE_MARGIN * 2));
update_vertical_tabs_resize_handle();
return;
}
update_vertical_tabs_resize_handle();
auto controls_width = m_new_tab_button->width();
if (m_minimize_window_button->isVisible())
controls_width += m_minimize_window_button->width();
@ -1221,11 +1414,7 @@ void TabWidget::update_tab_chrome_visibility()
void TabWidget::recreate_icons()
{
m_new_tab_button->setIcon(create_chrome_icon(ChromeIcon::NewTab, palette()));
m_toggle_vertical_tabs_expanded_action->setIcon(create_chrome_icon(
WebView::Application::the().toggle_vertical_tabs_expanded_action().engaged()
? ChromeIcon::VerticalTabBarCollapse
: ChromeIcon::VerticalTabBarExpand,
palette()));
update_vertical_tabs_action_labels();
update_window_button_icons();
}
@ -1238,6 +1427,7 @@ void TabWidget::update_chrome_style()
auto style_sheet = ChromeStyle::tab_widget_style_sheet(palette());
m_tab_bar_row->setStyleSheet(style_sheet);
m_vertical_tab_bar_column->setStyleSheet(style_sheet);
m_vertical_tabs_resize_handle->setStyleSheet(style_sheet);
m_is_updating_chrome_style = false;
}

View file

@ -81,6 +81,7 @@ private:
int drop_indicator_index_for_insertion_index(int insertion_index) const;
QPixmap render_tab_drag_pixmap(int index) const;
void toggle_window_maximized();
bool start_window_move();
void start_tab_drag(int index);
void start_hover_animation(int tab_index, qreal target_progress);
@ -165,6 +166,12 @@ private:
void rebuild_layout();
void rebuild_layout_for_horizontal_tabs();
void rebuild_layout_for_vertical_tabs();
int current_vertical_tabs_width() const;
void apply_vertical_tabs_expanded_width(int width);
void persist_vertical_tabs_expanded_width();
void update_vertical_tabs_resize_handle();
void set_resize_handle_property(char const* property, bool enabled);
void update_vertical_tabs_action_labels();
void update_tab_layout();
void update_tab_chrome_visibility();
void recreate_icons();
@ -177,23 +184,27 @@ private:
TabBar* m_tab_bar { nullptr };
QStackedWidget* m_stacked_widget { nullptr };
QToolButton* m_new_tab_button { nullptr };
QToolButton* m_toggle_vertical_tabs_expanded_button { nullptr };
QToolButton* m_minimize_window_button { nullptr };
QToolButton* m_maximize_window_button { nullptr };
QToolButton* m_close_window_button { nullptr };
QWidget* m_tab_bar_row { nullptr };
QWidget* m_vertical_tabs_new_tab_separator { nullptr };
QWidget* m_vertical_tab_bar_column { nullptr };
QWidget* m_vertical_tabs_resize_handle { nullptr };
QWidget* m_vertical_tabs_content { nullptr };
QBoxLayout* m_main_layout { nullptr };
QBoxLayout* m_tab_bar_row_layout { nullptr };
QBoxLayout* m_vertical_tab_bar_column_layout { nullptr };
QBoxLayout* m_vertical_tabs_content_layout { nullptr };
QAction* m_toggle_vertical_tabs_expanded_action { nullptr };
bool m_tab_bar_visible { true };
bool m_window_controls_visible { true };
bool m_vertical_tabs_enabled { false };
bool m_vertical_tabs_expanded { true };
bool m_is_resizing_vertical_tabs { false };
bool m_is_updating_chrome_style { false };
int m_vertical_tabs_expanded_width { 0 };
int m_vertical_tabs_resize_start_global_x { 0 };
int m_vertical_tabs_resize_start_width { 0 };
};
class TabBarButton final : public QPushButton {