UI/Qt: Unify browser chrome layout

Treat the toolbar, tab strip, and vertical sidebar as one browser
chrome surface. Add an explicit layout policy for platform window
controls and sidebar widths. This lets macOS use left-side traffic
lights while other platforms keep right-side custom controls.

Keep the macOS toolbar stable in vertical-tabs mode. Traffic lights and
navigation controls stay in the top row, and the sidebar sits below it.
Draw chrome/content separators explicitly so the content edge remains
clear without adding seams inside the chrome surface.

Retune tab, toolbar, and omnibox styling for the unified chrome. Tabs
use card-like selected states, quiet inactive states, and matching
horizontal and vertical geometry.

Make hover-expanded vertical tabs draw as a chrome overlay so the web
content does not relayout. Keep the overlay above native web views and
route macOS window drags through AppKit events for reliable dragging
from custom chrome.
This commit is contained in:
Andreas Kling 2026-06-02 17:45:44 +02:00 committed by Andreas Kling
parent 475d8f3c4b
commit 4f314b0aa0
15 changed files with 1090 additions and 306 deletions

View file

@ -9,6 +9,9 @@
#include <UI/Qt/Application.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/EventLoopImplementationQt.h>
#if defined(AK_OS_MACOS)
# include <UI/Qt/MacWindow.h>
#endif
#include <UI/Qt/Settings.h>
#include <UI/Qt/StringUtils.h>
#include <UI/Qt/WebContentView.h>
@ -63,6 +66,9 @@ public:
explicit LadybirdQApplication(Main::Arguments& arguments)
: QApplication(arguments.argc, arguments.argv)
{
#if defined(AK_OS_MACOS)
install_appkit_event_capture();
#endif
update_chrome_style();
}

View file

@ -18,6 +18,7 @@
#include <LibWebView/HistoryStore.h>
#include <UI/Qt/Application.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/ChromeLayout.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/DevToolsBanner.h>
#include <UI/Qt/Icon.h>
@ -362,13 +363,15 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
view_menu->addMenu(create_application_menu(*view_menu, Application::the().motion_menu()));
view_menu->addSeparator();
auto* show_menubar = new QAction("Show &Menubar", this);
show_menubar->setCheckable(true);
show_menubar->setChecked(Settings::the()->show_menubar());
view_menu->addAction(show_menubar);
QObject::connect(show_menubar, &QAction::triggered, this, [](bool checked) {
Settings::the()->set_show_menubar(checked);
});
if (show_menubar_option_available()) {
auto* show_menubar = new QAction("Show &Menubar", this);
show_menubar->setCheckable(true);
show_menubar->setChecked(Settings::the()->show_menubar());
view_menu->addAction(show_menubar);
QObject::connect(show_menubar, &QAction::triggered, this, [](bool checked) {
Settings::the()->set_show_menubar(checked);
});
}
m_bookmarks_menu = create_application_menu(*this, Application::the().bookmarks_menu());
m_hamburger_menu->addMenu(m_bookmarks_menu);
@ -896,21 +899,14 @@ void BrowserWindow::update_tab_button_icons()
void BrowserWindow::create_menu_bar_window_controls()
{
m_menu_bar_window_controls = new QWidget(menuBar());
m_menu_bar_window_controls->setObjectName("LadybirdMenuBarWindowControls");
auto* layout = new QHBoxLayout(m_menu_bar_window_controls);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_menu_bar_minimize_window_button = new WindowControlButton("LadybirdWindowButton", "Minimize", { 16, 16 }, { 40, 30 }, m_menu_bar_window_controls);
m_menu_bar_maximize_window_button = new WindowControlButton("LadybirdWindowButton", "Maximize", { 16, 16 }, { 40, 30 }, m_menu_bar_window_controls);
m_menu_bar_close_window_button = new WindowControlButton("LadybirdCloseWindowButton", "Close", { 16, 16 }, { 40, 30 }, m_menu_bar_window_controls);
layout->addWidget(m_menu_bar_minimize_window_button);
layout->addWidget(m_menu_bar_maximize_window_button);
layout->addWidget(m_menu_bar_close_window_button);
if (use_left_traffic_light_window_controls())
return;
auto window_control_buttons = create_window_control_buttons(*menuBar(), "LadybirdMenuBarWindowControls", { 16, 16 }, { 40, 30 });
m_menu_bar_window_controls = window_control_buttons.container;
m_menu_bar_minimize_window_button = window_control_buttons.minimize;
m_menu_bar_maximize_window_button = window_control_buttons.maximize;
m_menu_bar_close_window_button = window_control_buttons.close;
menuBar()->setCornerWidget(m_menu_bar_window_controls, Qt::TopRightCorner);
connect(m_menu_bar_minimize_window_button, &QToolButton::clicked, this, [this] {
@ -965,6 +961,10 @@ bool BrowserWindow::start_window_move()
auto* handle = windowHandle();
if (!handle)
return false;
#if defined(AK_OS_MACOS)
if (start_appkit_window_drag(*this))
return true;
#endif
return handle->startSystemMove();
}
@ -1306,11 +1306,11 @@ void BrowserWindow::config_variable_changed(WebView::ConfigVariableID variable)
void BrowserWindow::update_window_corners()
{
auto should_use_rounded_corners = WebView::Application::settings().config_variable_as_bool(WebView::ConfigVariableID::UseRoundedWindowCorners);
auto should_round_window = should_use_rounded_corners && !isMaximized() && !isFullScreen();
auto should_round_window = should_use_rounded_corners && !isFullScreen();
#if defined(AK_OS_MACOS)
clearMask();
set_rounded_window_corners(*this, should_round_window, WINDOW_CORNER_RADIUS);
set_rounded_window_corners(*this, should_round_window, WINDOW_CORNER_RADIUS, ChromeStyle::chrome_background(palette()));
#else
clearMask();
(void)should_round_window;

54
UI/Qt/ChromeLayout.h Normal file
View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Platform.h>
namespace Ladybird {
enum class WindowControlsPlacement {
LeftTrafficLights,
RightCustomControls,
};
struct BrowserChromeLayoutPolicy {
WindowControlsPlacement controls_placement;
int expanded_sidebar_width;
int collapsed_sidebar_width;
int toolbar_height;
};
static constexpr BrowserChromeLayoutPolicy browser_chrome_layout_policy()
{
return {
#if defined(AK_OS_MACOS)
.controls_placement = WindowControlsPlacement::LeftTrafficLights,
#else
.controls_placement = WindowControlsPlacement::RightCustomControls,
#endif
.expanded_sidebar_width = 232,
.collapsed_sidebar_width = 52,
.toolbar_height = 42,
};
}
static constexpr bool use_left_traffic_light_window_controls()
{
return browser_chrome_layout_policy().controls_placement == WindowControlsPlacement::LeftTrafficLights;
}
static constexpr bool use_right_custom_window_controls()
{
return browser_chrome_layout_policy().controls_placement == WindowControlsPlacement::RightCustomControls;
}
static constexpr bool show_menubar_option_available()
{
return use_right_custom_window_controls();
}
}

View file

@ -7,6 +7,7 @@
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/StringUtils.h>
#include <AK/Platform.h>
#include <QGuiApplication>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
# include <QStyleHints>
@ -124,19 +125,7 @@ QColor chrome_background(QPalette const& palette)
{
auto window = chrome_window(palette);
auto dark = is_dark(palette);
return mix(window, material_color_anchors(dark).background, dark ? 0.72 : 0.68);
}
static QColor chrome_tab_strip_background(QPalette const& palette)
{
auto background = chrome_background(palette);
auto dark = is_dark(palette);
return mix(background, dark ? QColor(13, 18, 26) : material_color_anchors(dark).recessed, dark ? 0.18 : 0.24);
}
static QColor chrome_tab_strip_background_bottom(QPalette const& palette)
{
return mix(chrome_tab_strip_background(palette), QColor(8, 11, 15), is_dark(palette) ? 0.22 : 0.026);
return mix(window, material_color_anchors(dark).background, dark ? 0.34 : 0.68);
}
QColor chrome_surface(QPalette const& palette)
@ -191,17 +180,21 @@ QColor chrome_control_border(QPalette const& palette)
QColor chrome_active_tab_surface_top(QPalette const& palette)
{
auto dark = is_dark(palette);
auto surface = chrome_surface(palette);
auto active_surface = chrome_surface_hover(palette);
return dark ? active_surface.lighter(108) : mix(surface, active_surface, 0.22);
if (!dark)
return QColor(255, 255, 255);
auto background = chrome_background(palette);
return mix(background, QColor(255, 255, 255), 0.22);
}
QColor chrome_active_tab_surface_bottom(QPalette const& palette)
{
auto dark = is_dark(palette);
auto surface = chrome_surface(palette);
auto active_surface = chrome_surface_hover(palette);
return dark ? mix(surface, active_surface, 0.66) : mix(chrome_background(palette), active_surface, 0.70);
if (!dark)
return QColor(251, 251, 251);
auto background = chrome_background(palette);
return mix(background, QColor(255, 255, 255), 0.20);
}
QColor chrome_border(QPalette const& palette)
@ -298,7 +291,17 @@ QMenu::item:disabled {{
QMenu::icon {{
left: 8px;
}}
)"
#if defined(AK_OS_MACOS)
R"(
QMenu::right-arrow,
QMenu::left-arrow {{
width: 8px;
height: 8px;
}}
)"
#endif
R"(
QMenu::separator {{
background: {4};
height: 1px;
@ -310,16 +313,12 @@ QMenu::separator {{
QString toolbar_container_style_sheet(QPalette const& palette)
{
auto dark = is_dark(palette);
auto background = style_sheet_color(chrome_active_tab_surface_top(palette));
auto background_bottom = style_sheet_color(chrome_active_tab_surface_bottom(palette));
auto background = style_sheet_color(chrome_background(palette));
auto surface_hover = style_sheet_color(chrome_control_surface_hover(palette));
auto surface_pressed = style_sheet_color(chrome_control_surface_pressed(palette));
auto control_border = style_sheet_color(chrome_control_border(palette));
auto separator = dark
? background_bottom
: style_sheet_color(mix(chrome_background(palette), chrome_border(palette), 0.56));
auto window_controls_separator = style_sheet_color(mix(chrome_active_tab_surface_bottom(palette), chrome_border(palette), dark ? 0.36 : 0.46));
auto separator = style_sheet_color(chrome_border(palette));
auto window_controls_separator = style_sheet_color(mix(chrome_background(palette), chrome_border(palette), is_dark(palette) ? 0.36 : 0.46));
auto text = style_sheet_color(chrome_button_text(palette));
auto disabled_text = style_sheet_color(chrome_muted_text(palette));
auto close_hover = style_sheet_color(chrome_destructive_hover());
@ -327,13 +326,17 @@ QString toolbar_container_style_sheet(QPalette const& palette)
return qformatted(R"(
QWidget#LadybirdToolbarContainer {{
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {0}, stop:1 {1});
background: {0};
border: 0;
border-bottom: 1px solid {5};
border-bottom: 1px solid {4};
}}
QWidget#LadybirdToolbarContainer[fullWidthToolbar="true"] {{
border-bottom: 0;
}}
QWidget#LadybirdNavigationToolbar QToolButton {{
color: {6};
color: {5};
background: transparent;
border: 1px solid transparent;
border-radius: 17px;
@ -344,18 +347,18 @@ QWidget#LadybirdNavigationToolbar QToolButton {{
}}
QWidget#LadybirdNavigationToolbar QToolButton:hover {{
background: {2};
border-color: {4};
background: {1};
border-color: {3};
}}
QWidget#LadybirdNavigationToolbar QToolButton:pressed,
QWidget#LadybirdNavigationToolbar QToolButton:checked {{
background: {3};
border-color: {4};
background: {2};
border-color: {3};
}}
QWidget#LadybirdNavigationToolbar QToolButton:disabled {{
color: {7};
color: {6};
background: transparent;
border-color: transparent;
}}
@ -365,12 +368,12 @@ QWidget#LadybirdNavigationToolbar QToolButton::menu-indicator {{
}}
QWidget#LadybirdToolbarWindowControlsSeparator {{
background: {10};
background: {9};
}}
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdWindowButton,
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdCloseWindowButton {{
color: {6};
color: {5};
background: transparent;
border: 0;
border-radius: 0;
@ -383,36 +386,36 @@ QWidget#LadybirdNavigationToolbar QToolButton#LadybirdCloseWindowButton {{
}}
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdWindowButton:hover {{
background: {2};
background: {1};
}}
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdWindowButton:pressed {{
background: {3};
background: {2};
}}
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdCloseWindowButton:hover {{
color: {9};
background: {8};
color: {8};
background: {7};
}}
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdCloseWindowButton:pressed {{
color: {9};
background: {8};
color: {8};
background: {7};
}}
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdWindowButton[pressedOutside="true"],
QWidget#LadybirdNavigationToolbar QToolButton#LadybirdCloseWindowButton[pressedOutside="true"] {{
color: {6};
color: {5};
background: transparent;
}}
)",
background, background_bottom, surface_hover, surface_pressed, control_border, separator, text, disabled_text, close_hover, close_text, window_controls_separator);
background, surface_hover, surface_pressed, control_border, separator, text, disabled_text, close_hover, close_text, window_controls_separator);
}
QString menu_bar_style_sheet(QPalette const& palette)
{
auto background = style_sheet_color(chrome_tab_strip_background(palette));
auto background_bottom = style_sheet_color(chrome_tab_strip_background_bottom(palette));
auto background = style_sheet_color(chrome_background(palette));
auto background_bottom = background;
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));
@ -500,24 +503,22 @@ QString location_edit_style_sheet(QPalette const& palette)
auto dark = is_dark(palette);
auto surface_color = chrome_surface(palette);
if (dark)
surface_color = mix(surface_color, material_color_anchors(true).background, 0.42);
surface_color = mix(chrome_background(palette), material_color_anchors(true).background, 0.34);
auto hover_color = chrome_surface_hover(palette);
if (dark)
hover_color = mix(hover_color, material_color_anchors(true).background, 0.24);
hover_color = mix(surface_color, QColor(255, 255, 255), 0.035);
auto border_color = dark ? mix(surface_color, chrome_background(palette), 0.56) : chrome_border(palette);
auto hover_border_color = dark ? mix(surface_color, chrome_border(palette), 0.20) : chrome_control_border(palette);
auto border_color = dark ? mix(chrome_background(palette), chrome_border(palette), 0.36) : chrome_border(palette);
auto hover_border_color = dark ? mix(chrome_background(palette), chrome_border(palette), 0.48) : chrome_control_border(palette);
auto focus_border_color = mix(chrome_border(palette), chrome_accent(palette), dark ? 0.50 : 0.54);
auto surface = style_sheet_color(surface_color);
auto hover = style_sheet_color(hover_color);
auto control_hover = style_sheet_color(chrome_control_surface_hover(palette));
auto control_pressed = style_sheet_color(chrome_control_surface_pressed(palette));
auto border = style_sheet_color(border_color);
auto hover_border = style_sheet_color(hover_border_color);
auto focus_border = style_sheet_color(focus_border_color);
auto text = style_sheet_color(chrome_text(palette));
auto placeholder = style_sheet_color(chrome_muted_text(palette));
auto text = style_sheet_color(dark ? mix(chrome_text(palette), QColor(255, 255, 255), 0.08) : chrome_text(palette));
auto placeholder = style_sheet_color(mix(chrome_muted_text(palette), surface_color, dark ? 0.46 : 0.34));
auto selection = style_sheet_color(chrome_accent(palette));
auto selection_text = style_sheet_color(palette.color(QPalette::HighlightedText));
auto not_secure_text = style_sheet_color(dark ? QColor(224, 142, 136) : QColor(144, 62, 56));
@ -541,11 +542,12 @@ QLineEdit#LadybirdLocationEdit {{
padding: 0 16px;
selection-background-color: {6};
selection-color: {7};
placeholder-text-color: {5};
}}
QLineEdit#LadybirdLocationEdit:hover {{
background: {1};
border-color: {10};
border-color: {8};
}}
QLineEdit#LadybirdLocationEdit:focus {{
@ -565,56 +567,47 @@ QToolButton#LadybirdLocationIcon {{
}}
QToolButton#LadybirdLocationIcon[notSecure="true"] {{
color: {11};
background: {12};
border: 1px solid {15};
color: {9};
background: {10};
border: 1px solid {13};
border-radius: 10px;
padding: 0 7px;
font-weight: 500;
}}
QToolButton#LadybirdLocationIcon[notSecure="true"]:hover {{
background: {13};
background: {11};
}}
QToolButton#LadybirdLocationIcon[notSecure="true"]:pressed {{
background: {14};
background: {12};
}}
QToolButton#LadybirdLocationZoomIndicator {{
color: {16};
background: {17};
border: 1px solid {20};
color: {14};
background: {15};
border: 1px solid {18};
border-radius: 10px;
padding: 0 7px;
font-weight: 500;
}}
QToolButton#LadybirdLocationZoomIndicator:hover {{
background: {18};
background: {16};
}}
QToolButton#LadybirdLocationZoomIndicator:pressed {{
background: {19};
background: {17};
}}
QToolButton#LadybirdLocationAction {{
background: transparent;
border: 0;
border-radius: 11px;
margin: 1px;
padding: 0;
}}
QToolButton#LadybirdLocationAction:hover {{
background: {8};
}}
QToolButton#LadybirdLocationAction:pressed {{
background: {9};
}}
)",
surface, hover, border, focus_border, text, placeholder, selection, selection_text, control_hover, control_pressed, hover_border,
surface, hover, border, focus_border, text, placeholder, selection, selection_text, hover_border,
not_secure_text, not_secure_background, not_secure_hover, not_secure_pressed, not_secure_border, zoom_text, zoom_background, zoom_hover, zoom_pressed, zoom_border);
}
@ -749,38 +742,35 @@ QWidget#LadybirdDevToolsBanner QPushButton:pressed {{
QString tab_widget_style_sheet(QPalette const& palette)
{
auto dark = is_dark(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 chrome_background_color = chrome_background(palette);
auto background = style_sheet_color(chrome_background_color);
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));
auto text = style_sheet_color(chrome_button_text(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));
auto strip_separator = style_sheet_color(chrome_border(palette));
auto sidebar_separator = style_sheet_color(mix(chrome_background_color, chrome_border(palette), dark ? 0.44 : 0.58));
auto sidebar_separator_hover = style_sheet_color(mix(chrome_background_color, chrome_border(palette), dark ? 0.64 : 0.76));
auto vertical_tab_button_background_color = style_sheet_color(chrome_active_tab_surface_top(palette));
return qformatted(R"(
QWidget#LadybirdTabStrip {{
color: {5};
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {0}, stop:1 {1});
color: {4};
background: {0};
border: 0;
border-bottom: 1px solid {8};
border-bottom: 1px solid {7};
}}
QWidget#LadybirdVerticalTabBar {{
color: {5};
background: {11};
border-right: 1px solid {9};
color: {4};
background: {0};
border-right: 1px solid {8};
}}
QWidget#LadybirdVerticalTabBar[hovered="true"],
QWidget#LadybirdVerticalTabBar[active="true"] {{
border-right: 1px solid {10};
border-right: 1px solid {9};
}}
QWidget#LadybirdVerticalTabsResizeHandle {{
@ -788,8 +778,15 @@ QWidget#LadybirdVerticalTabsResizeHandle {{
border: 0;
}}
QWidget#LadybirdVerticalTabsContentSeparator {{
background: {7};
border: 0;
min-height: 1px;
max-height: 1px;
}}
QWidget#LadybirdVerticalTabsSeparator {{
background: {9};
background: {8};
min-height: 1px;
max-height: 1px;
}}
@ -797,7 +794,7 @@ QWidget#LadybirdVerticalTabsSeparator {{
QPushButton#LadybirdAudioState,
QToolButton#LadybirdNewTabButton,
QPushButton#LadybirdTabButton {{
color: {5};
color: {4};
background: transparent;
border: 1px solid transparent;
border-radius: 11px;
@ -831,32 +828,32 @@ QPushButton#LadybirdTabButton[collapsedVerticalTabButton="true"] {{
min-height: 16px;
max-width: 16px;
max-height: 16px;
background: {12};
background: {10};
border-color: {4};
border-radius: 8px;
}}
QPushButton#LadybirdAudioState:hover,
QToolButton#LadybirdNewTabButton:hover,
QToolButton#LadybirdNewTabButton[verticalTabsButton="false"]:hover,
QPushButton#LadybirdTabButton:hover {{
color: {5};
background: {2};
border-color: {4};
color: {4};
background: {1};
border-color: {3};
}}
QPushButton#LadybirdAudioState:pressed,
QPushButton#LadybirdAudioState:checked,
QToolButton#LadybirdNewTabButton:pressed,
QToolButton#LadybirdNewTabButton[verticalTabsButton="false"]:pressed,
QPushButton#LadybirdTabButton:pressed,
QPushButton#LadybirdTabButton:checked {{
color: {5};
background: {3};
border-color: {4};
color: {4};
background: {2};
border-color: {3};
}}
QToolButton#LadybirdWindowButton,
QToolButton#LadybirdCloseWindowButton {{
color: {5};
color: {4};
background: transparent;
border: 0;
border-radius: 0;
@ -866,31 +863,31 @@ QToolButton#LadybirdCloseWindowButton {{
}}
QToolButton#LadybirdWindowButton:hover {{
background: {2};
background: {1};
}}
QToolButton#LadybirdWindowButton:pressed {{
background: {3};
background: {2};
}}
QToolButton#LadybirdCloseWindowButton:hover {{
color: {7};
background: {6};
color: {6};
background: {5};
}}
QToolButton#LadybirdCloseWindowButton:pressed {{
color: {7};
background: {6};
color: {6};
background: {5};
}}
QToolButton#LadybirdWindowButton[pressedOutside="true"],
QToolButton#LadybirdCloseWindowButton[pressedOutside="true"] {{
color: {5};
color: {4};
background: transparent;
}}
)",
background, background_bottom, hover, pressed, control_border, text, close_hover, close_text, strip_separator,
sidebar_separator, sidebar_separator_hover, sidebar_background, vertical_tab_button_background_color);
background, hover, pressed, control_border, text, close_hover, close_text, strip_separator,
sidebar_separator, sidebar_separator_hover, vertical_tab_button_background_color);
}
QString autocomplete_popup_style_sheet(QPalette const& palette)

View file

@ -48,22 +48,22 @@ static void draw_stroked_icon_path(QPainter& painter, QPainterPath const& path,
static void draw_back_icon(QPainter& painter, QColor const& color)
{
QPainterPath path;
path.moveTo(18.2, 10.3);
path.lineTo(4.8, 10.3);
path.moveTo(10.7, 5.0);
path.lineTo(4.8, 10.3);
path.lineTo(10.7, 15.6);
path.moveTo(17.2, 10.3);
path.lineTo(3.8, 10.3);
path.moveTo(9.7, 5.0);
path.lineTo(3.8, 10.3);
path.lineTo(9.7, 15.6);
draw_stroked_icon_path(painter, path, color, 2.0);
}
static void draw_forward_icon(QPainter& painter, QColor const& color)
{
QPainterPath path;
path.moveTo(1.8, 10.3);
path.lineTo(15.2, 10.3);
path.moveTo(9.3, 5.0);
path.lineTo(15.2, 10.3);
path.lineTo(9.3, 15.6);
path.moveTo(2.8, 10.3);
path.lineTo(16.2, 10.3);
path.moveTo(10.3, 5.0);
path.lineTo(16.2, 10.3);
path.lineTo(10.3, 15.6);
draw_stroked_icon_path(painter, path, color, 2.0);
}
@ -89,16 +89,16 @@ static void draw_reload_icon(QPainter& painter, QColor const& color)
static void draw_star_icon(QPainter& painter, QColor const& color, bool filled)
{
QPainterPath path;
path.moveTo(10.0, 2.6);
path.lineTo(12.5, 7.5);
path.lineTo(18.0, 8.0);
path.lineTo(13.9, 11.7);
path.lineTo(15.1, 17.2);
path.lineTo(10.0, 14.3);
path.lineTo(4.9, 17.2);
path.lineTo(6.1, 11.7);
path.lineTo(2.0, 8.0);
path.lineTo(7.5, 7.5);
path.moveTo(10.0, 4.1);
path.lineTo(12.5, 9.0);
path.lineTo(18.0, 9.5);
path.lineTo(13.9, 13.2);
path.lineTo(15.1, 18.7);
path.lineTo(10.0, 15.8);
path.lineTo(4.9, 18.7);
path.lineTo(6.1, 13.2);
path.lineTo(2.0, 9.5);
path.lineTo(7.5, 9.0);
path.closeSubpath();
painter.setPen(chrome_icon_pen(color, filled ? 1.2 : 1.65));

View file

@ -25,6 +25,7 @@
#include <QKeyEvent>
#include <QLatin1String>
#include <QMouseEvent>
#include <QPainter>
#include <QPalette>
#include <QResizeEvent>
#include <QStyle>
@ -38,6 +39,43 @@
namespace Ladybird {
class LocationActionButton final : public QToolButton {
public:
explicit LocationActionButton(QWidget* parent)
: QToolButton(parent)
{
}
private:
virtual void paintEvent(QPaintEvent*) override
{
static constexpr int hover_size = 23;
static constexpr int icon_y_offset = -1;
static constexpr qreal hover_y_offset = 1.0;
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
if (isDown() || underMouse()) {
auto background = isDown()
? ChromeStyle::mix(ChromeStyle::chrome_surface_pressed(palette()), ChromeStyle::chrome_button_text(palette()), 0.04)
: ChromeStyle::mix(ChromeStyle::chrome_surface_hover(palette()), ChromeStyle::chrome_button_text(palette()), 0.04);
painter.setPen(Qt::NoPen);
painter.setBrush(background);
auto hover_rect = QRectF(0, 0, hover_size, hover_size);
hover_rect.moveCenter(QPointF(rect().center().x(), rect().center().y() + hover_y_offset));
painter.drawRoundedRect(hover_rect, 10, 10);
}
auto icon_rect = QRect(
(width() - iconSize().width()) / 2,
(height() - iconSize().height()) / 2 + icon_y_offset,
iconSize().width(),
iconSize().height());
icon().paint(&painter, icon_rect, Qt::AlignCenter, isEnabled() ? QIcon::Normal : QIcon::Disabled, isDown() ? QIcon::On : QIcon::Off);
}
};
static QColor location_focus_glow_color(QPalette const& palette, int alpha)
{
auto color = ChromeStyle::chrome_accent(palette);
@ -152,7 +190,8 @@ static bool should_suppress_inline_autocomplete_for_key(QKeyEvent const* event)
static constexpr int LOCATION_TRAILING_EDGE_MARGIN = 12;
static constexpr int LOCATION_TRAILING_TEXT_GAP = 4;
static constexpr int LOCATION_TRAILING_ITEM_GAP = 6;
static constexpr int LOCATION_TRAILING_ACTION_SIZE = 24;
static constexpr int LOCATION_TRAILING_ACTION_WIDTH = 24;
static constexpr int LOCATION_TRAILING_ACTION_HEIGHT = 23;
static constexpr int LOCATION_PILL_HEIGHT = 22;
static constexpr int LOCATION_PILL_HORIZONTAL_PADDING = 18;
@ -186,11 +225,11 @@ LocationEdit::LocationEdit(QWidget* parent)
m_leading_icon_button->setCursor(Qt::ArrowCursor);
m_leading_icon_button->hide();
m_trailing_action_button = new QToolButton(this);
m_trailing_action_button = new LocationActionButton(this);
m_trailing_action_button->setObjectName("LadybirdLocationAction");
m_trailing_action_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_trailing_action_button->setIconSize({ 18, 18 });
m_trailing_action_button->setFixedSize(24, 24);
m_trailing_action_button->setIconSize({ 17, 17 });
m_trailing_action_button->setFixedSize(LOCATION_TRAILING_ACTION_WIDTH, LOCATION_TRAILING_ACTION_HEIGHT);
m_trailing_action_button->setAutoRaise(true);
m_trailing_action_button->setFocusPolicy(Qt::NoFocus);
m_trailing_action_button->setCursor(Qt::ArrowCursor);
@ -367,8 +406,12 @@ void LocationEdit::focusInEvent(QFocusEvent* event)
highlight_location();
animate_focus_glow(58);
if (event->reason() != Qt::PopupFocusReason && !should_defer_full_url)
QTimer::singleShot(0, this, &QLineEdit::selectAll);
if (event->reason() != Qt::PopupFocusReason && !should_defer_full_url) {
QTimer::singleShot(0, this, [this] {
if (hasFocus())
selectAll();
});
}
}
void LocationEdit::focusOutEvent(QFocusEvent* event)
@ -390,6 +433,7 @@ void LocationEdit::focusOutEvent(QFocusEvent* event)
setText(display_url());
}
deselect();
if (event->reason() != Qt::PopupFocusReason) {
setCursorPosition(0);
highlight_location();
@ -474,7 +518,7 @@ void LocationEdit::update_trailing_item_positions()
auto trailing_button_size = m_trailing_action_button->size();
auto trailing_x = width() - trailing_button_size.width() - LOCATION_TRAILING_EDGE_MARGIN;
auto trailing_y = (height() - trailing_button_size.height()) / 2 + 1;
auto trailing_y = (height() - trailing_button_size.height()) / 2;
m_trailing_action_button->move(trailing_x, trailing_y);
auto zoom_button_size = m_zoom_indicator_button->size();
@ -497,7 +541,7 @@ void LocationEdit::update_text_margins()
int LocationEdit::trailing_text_margin() const
{
auto margin = LOCATION_TRAILING_EDGE_MARGIN + LOCATION_TRAILING_ACTION_SIZE + LOCATION_TRAILING_TEXT_GAP;
auto margin = LOCATION_TRAILING_EDGE_MARGIN + LOCATION_TRAILING_ACTION_WIDTH + LOCATION_TRAILING_TEXT_GAP;
if (m_zoom_indicator_button && !m_zoom_indicator_button->isHidden())
margin += m_zoom_indicator_button->width() + LOCATION_TRAILING_ITEM_GAP;
@ -603,7 +647,11 @@ void LocationEdit::update_location_icon()
update_text_margins();
};
if (text_matches_current_url()) {
auto is_showing_current_url_for_display = !hasFocus()
&& m_url.has_value()
&& text() == display_url();
if (text_matches_current_url() || is_showing_current_url_for_display) {
auto const& scheme = m_url->scheme();
if (scheme == "http"sv)
show_not_secure_indicator();

View file

@ -7,9 +7,13 @@
#pragma once
class QWidget;
class QColor;
namespace Ladybird {
void set_rounded_window_corners(QWidget&, bool enabled, double radius);
void set_rounded_window_corners(QWidget&, bool enabled, double radius, QColor const& background_color);
void install_always_active_window_control_hover_tracking(QWidget&, void (*hover_changed)(QWidget*));
void install_appkit_event_capture();
bool start_appkit_window_drag(QWidget&);
}

View file

@ -6,14 +6,122 @@
#include <UI/Qt/MacWindow.h>
#include <QAbstractNativeEventFilter>
#include <QColor>
#include <QCoreApplication>
#include <QPoint>
#include <QPointer>
#include <QRect>
#include <QWidget>
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
@interface LadybirdWindowControlHoverTracker : NSObject
@property (nonatomic, retain) NSTrackingArea* trackingArea;
- (instancetype)initWithWidget:(QWidget*)widget callback:(void (*)(QWidget*))callback;
@end
@implementation LadybirdWindowControlHoverTracker
{
void (*m_callback)(QWidget*);
QPointer<QWidget> m_widget;
}
- (instancetype)initWithWidget:(QWidget*)widget callback:(void (*)(QWidget*))callback
{
if ((self = [super init])) {
m_callback = callback;
m_widget = widget;
}
return self;
}
- (void)mouseEntered:(NSEvent*)event
{
(void)event;
if (m_callback && m_widget)
m_callback(m_widget);
}
- (void)mouseExited:(NSEvent*)event
{
(void)event;
if (m_callback && m_widget)
m_callback(m_widget);
}
#if !__has_feature(objc_arc)
- (void)dealloc
{
[_trackingArea release];
[super dealloc];
}
#endif
@end
namespace Ladybird {
void set_rounded_window_corners(QWidget& widget, bool enabled, double radius)
static NSEvent* s_latest_window_drag_event;
static bool is_appkit_event_type(QByteArray const& event_type)
{
return event_type == QByteArrayLiteral("NSEvent")
|| event_type == QByteArrayLiteral("mac_generic_NSEvent");
}
static bool is_window_drag_event(NSEvent* event)
{
if (!event)
return false;
switch (event.type) {
case NSEventTypeLeftMouseDown:
case NSEventTypeLeftMouseDragged:
return true;
default:
return false;
}
}
class LadybirdAppKitEventCaptureFilter final : public QAbstractNativeEventFilter {
public:
virtual bool nativeEventFilter(QByteArray const& event_type, void* message, qintptr*) override
{
if (!is_appkit_event_type(event_type))
return false;
auto* event = static_cast<NSEvent*>(message);
if (!is_window_drag_event(event))
return false;
[s_latest_window_drag_event release];
s_latest_window_drag_event = [event copy];
return false;
}
};
static CGColorRef cg_color_from_qcolor(QColor const& color)
{
return [NSColor colorWithSRGBRed:color.redF() green:color.greenF() blue:color.blueF() alpha:1.0].CGColor;
}
void install_appkit_event_capture()
{
static auto* filter = new LadybirdAppKitEventCaptureFilter;
static bool installed = false;
if (installed)
return;
if (auto* application = QCoreApplication::instance()) {
application->installNativeEventFilter(filter);
installed = true;
}
}
void set_rounded_window_corners(QWidget& widget, bool enabled, double radius, QColor const& background_color)
{
auto* view = reinterpret_cast<NSView*>(widget.winId());
if (!view)
@ -30,6 +138,68 @@ void set_rounded_window_corners(QWidget& widget, bool enabled, double radius)
content_view.wantsLayer = YES;
content_view.layer.cornerRadius = enabled ? radius : 0.0;
content_view.layer.masksToBounds = enabled ? YES : NO;
content_view.layer.opaque = YES;
content_view.layer.backgroundColor = cg_color_from_qcolor(background_color);
}
void install_always_active_window_control_hover_tracking(QWidget& widget, void (*hover_changed)(QWidget*))
{
static char tracker_key;
auto* window_widget = widget.window();
if (!window_widget)
return;
auto* view = reinterpret_cast<NSView*>(window_widget->winId());
if (!view)
return;
if (auto* existing_tracker = static_cast<LadybirdWindowControlHoverTracker*>(objc_getAssociatedObject(view, &tracker_key))) {
if (existing_tracker.trackingArea)
[view removeTrackingArea:existing_tracker.trackingArea];
}
auto* tracker = [[LadybirdWindowControlHoverTracker alloc] initWithWidget:&widget callback:hover_changed];
auto const widget_rect = QRect(widget.mapTo(window_widget, QPoint(0, 0)), widget.size());
auto tracking_rect = NSMakeRect(widget_rect.x(), widget_rect.y(), widget_rect.width(), widget_rect.height());
if (![view isFlipped])
tracking_rect.origin.y = NSHeight(view.bounds) - NSMaxY(tracking_rect);
auto options = NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways;
auto* tracking_area = [[NSTrackingArea alloc] initWithRect:tracking_rect options:options owner:tracker userInfo:nil];
tracker.trackingArea = tracking_area;
[view addTrackingArea:tracking_area];
objc_setAssociatedObject(view, &tracker_key, tracker, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
#if !__has_feature(objc_arc)
[tracking_area release];
[tracker release];
#endif
}
bool start_appkit_window_drag(QWidget& widget)
{
auto* window_widget = widget.window();
if (!window_widget)
return false;
auto* view = reinterpret_cast<NSView*>(window_widget->winId());
if (!view)
return false;
auto* window = view.window;
if (!window)
return false;
auto* current_event = NSApp.currentEvent;
auto current_event_is_valid = is_window_drag_event(current_event);
auto current_event_window_matches = current_event_is_valid && current_event.window == window;
auto* event = current_event_window_matches ? current_event : s_latest_window_drag_event;
if (!is_window_drag_event(event) || event.window != window)
return false;
[window performWindowDragWithEvent:event];
return true;
}
}

View file

@ -7,6 +7,7 @@
*/
#include <AK/LexicalPath.h>
#include <UI/Qt/ChromeLayout.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/StringUtils.h>
@ -56,12 +57,19 @@ void Settings::set_is_maximized(bool is_maximized)
bool Settings::show_menubar()
{
if (!show_menubar_option_available())
return false;
return m_qsettings->value("show_menubar", false).toBool();
}
void Settings::set_show_menubar(bool show_menubar)
{
m_qsettings->setValue("show_menubar", show_menubar);
if (!show_menubar_option_available())
show_menubar = false;
else
m_qsettings->setValue("show_menubar", show_menubar);
emit show_menubar_changed(show_menubar);
}

View file

@ -13,6 +13,7 @@
#include <LibWebView/Utilities.h>
#include <LibWebView/WebContentClient.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/ChromeLayout.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
@ -90,6 +91,8 @@ static QToolButton* create_toolbar_button(QWidget& parent, QAction& action)
static constexpr int TOOLBAR_HORIZONTAL_MARGIN = 12;
static constexpr int TOOLBAR_VERTICAL_MARGIN = 2;
static constexpr int TOOLBAR_MACOS_TRAFFIC_LIGHTS_CONTROL_GAP = 22;
static constexpr int TOOLBAR_SIDEBAR_TOGGLE_NAVIGATION_GAP = 8;
static constexpr int TOOLBAR_WINDOW_CONTROLS_RIGHT_MARGIN = 4;
Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client, size_t page_index)
@ -117,7 +120,7 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
m_toolbar = new QWidget(this);
m_toolbar->setObjectName("LadybirdNavigationToolbar");
m_toolbar->setFixedHeight(42);
m_toolbar->setFixedHeight(browser_chrome_layout_policy().toolbar_height);
m_toolbar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
auto* toolbar_container_layout = new QVBoxLayout(m_toolbar_container);
@ -177,19 +180,11 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
m_toolbar_window_controls_separator->setObjectName("LadybirdToolbarWindowControlsSeparator");
m_toolbar_window_controls_separator->setFixedSize(1, 22);
m_toolbar_window_controls = new QWidget(m_toolbar);
m_toolbar_window_controls->setObjectName("LadybirdToolbarWindowControls");
auto* window_controls_layout = new QHBoxLayout(m_toolbar_window_controls);
window_controls_layout->setSpacing(0);
window_controls_layout->setContentsMargins(0, 0, 0, 0);
m_minimize_window_button = new WindowControlButton("LadybirdWindowButton", "Minimize", { 16, 16 }, { 38, 38 }, m_toolbar_window_controls);
m_maximize_window_button = new WindowControlButton("LadybirdWindowButton", "Maximize", { 16, 16 }, { 38, 38 }, m_toolbar_window_controls);
m_close_window_button = new WindowControlButton("LadybirdCloseWindowButton", "Close", { 16, 16 }, { 38, 38 }, m_toolbar_window_controls);
window_controls_layout->addWidget(m_minimize_window_button);
window_controls_layout->addWidget(m_maximize_window_button);
window_controls_layout->addWidget(m_close_window_button);
auto window_control_buttons = create_window_control_buttons(*m_toolbar, "LadybirdToolbarWindowControls", { 16, 16 }, { 38, 38 });
m_toolbar_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;
QObject::connect(m_minimize_window_button, &QToolButton::clicked, this, [this] {
m_window->showMinimized();
@ -212,24 +207,34 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
m_media_context_menu = create_context_menu(*this, view(), view().media_context_menu());
auto* navigation_button_cluster = new QWidget(m_toolbar);
navigation_button_cluster->setProperty(WINDOW_DRAG_REGION_PROPERTY, true);
auto* navigation_button_layout = new QHBoxLayout(navigation_button_cluster);
navigation_button_layout->setSpacing(2);
navigation_button_layout->setContentsMargins(0, 0, 0, 0);
navigation_button_layout->addWidget(create_toolbar_button(*navigation_button_cluster, *m_toggle_vertical_tabs_expanded_action));
m_sidebar_toggle_navigation_spacer = new QSpacerItem(0, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
navigation_button_layout->addItem(m_sidebar_toggle_navigation_spacer);
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));
if (use_left_traffic_light_window_controls()) {
toolbar_layout->addWidget(m_toolbar_window_controls, 0, Qt::AlignVCenter);
m_toolbar_window_controls_spacer = new QSpacerItem(TOOLBAR_MACOS_TRAFFIC_LIGHTS_CONTROL_GAP, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
toolbar_layout->addItem(m_toolbar_window_controls_spacer);
}
toolbar_layout->addWidget(navigation_button_cluster, 0, Qt::AlignTop);
m_location_edit->set_trailing_action(create_application_action(*m_location_edit, view().toggle_bookmark_action()));
m_location_edit->set_zoom_action(create_application_action(*m_location_edit, view().reset_zoom_action(), IncludeActionIcon::No));
toolbar_layout->addWidget(m_location_edit, 1);
toolbar_layout->addWidget(m_hamburger_button, 0, Qt::AlignTop);
toolbar_layout->addWidget(m_toolbar_window_controls_separator, 0, Qt::AlignVCenter);
toolbar_layout->addWidget(m_toolbar_window_controls, 0, Qt::AlignVCenter);
if (use_right_custom_window_controls()) {
toolbar_layout->addWidget(m_toolbar_window_controls_separator, 0, Qt::AlignVCenter);
toolbar_layout->addWidget(m_toolbar_window_controls, 0, Qt::AlignVCenter);
}
update_chrome_style();
set_toolbar_window_controls_visible(!Settings::the()->show_menubar() && WebView::Application::settings().tab_settings().vertical_tabs_enabled);
set_toolbar_window_controls_visible(false);
m_hamburger_button->setVisible(!Settings::the()->show_menubar());
@ -578,14 +583,43 @@ void Tab::set_window(BrowserWindow& window)
void Tab::set_vertical_tabs_enabled(bool enabled)
{
m_toolbar->setProperty(WINDOW_DRAG_REGION_PROPERTY, enabled);
m_toolbar->setProperty(WINDOW_DRAG_REGION_PROPERTY, true);
if (m_sidebar_toggle_navigation_spacer)
m_sidebar_toggle_navigation_spacer->changeSize(enabled ? TOOLBAR_SIDEBAR_TOGGLE_NAVIGATION_GAP : 0, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
m_toolbar->layout()->invalidate();
}
void Tab::set_toolbar_container_in_tab_layout(bool in_tab_layout)
{
auto* tab_layout = static_cast<QBoxLayout*>(layout());
auto index = tab_layout->indexOf(m_toolbar_container);
if (in_tab_layout) {
if (index == -1) {
m_toolbar_container->setParent(this);
tab_layout->insertWidget(0, m_toolbar_container);
}
m_toolbar_container->show();
return;
}
if (index != -1) {
tab_layout->removeWidget(m_toolbar_container);
m_toolbar_container->hide();
}
}
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);
auto const has_left_traffic_lights = use_left_traffic_light_window_controls() && visible;
auto const has_trailing_window_controls = use_right_custom_window_controls() && visible;
m_toolbar_window_controls_separator->setVisible(has_trailing_window_controls);
m_toolbar_window_controls->setVisible(has_left_traffic_lights || has_trailing_window_controls);
if (m_toolbar_window_controls_spacer)
m_toolbar_window_controls_spacer->changeSize(has_left_traffic_lights ? TOOLBAR_MACOS_TRAFFIC_LIGHTS_CONTROL_GAP : 0, 0, QSizePolicy::Fixed, QSizePolicy::Minimum);
m_toolbar->layout()->setContentsMargins(TOOLBAR_HORIZONTAL_MARGIN, TOOLBAR_VERTICAL_MARGIN, has_trailing_window_controls ? TOOLBAR_WINDOW_CONTROLS_RIGHT_MARGIN : TOOLBAR_HORIZONTAL_MARGIN, TOOLBAR_VERTICAL_MARGIN);
m_toolbar->layout()->invalidate();
}
void Tab::update_window_control_icons()

View file

@ -79,9 +79,11 @@ public:
QMenu* context_menu() const { return m_context_menu; }
QToolButton* hamburger_button() const { return m_hamburger_button; }
QWidget* toolbar_container() const { return m_toolbar_container; }
void set_vertical_tabs_enabled(bool);
void set_window(BrowserWindow&);
void set_toolbar_container_in_tab_layout(bool);
void set_toolbar_window_controls_visible(bool);
void update_window_control_icons();
void update_hover_label();
@ -115,6 +117,8 @@ private:
QWidget* m_toolbar { nullptr };
QWidget* m_toolbar_window_controls_separator { nullptr };
QWidget* m_toolbar_window_controls { nullptr };
QSpacerItem* m_toolbar_window_controls_spacer { nullptr };
QSpacerItem* m_sidebar_toggle_navigation_spacer { nullptr };
WindowControlButton* m_minimize_window_button { nullptr };
WindowControlButton* m_maximize_window_button { nullptr };
WindowControlButton* m_close_window_button { nullptr };

View file

@ -9,8 +9,12 @@
#include <AK/StdLibExtras.h>
#include <UI/Qt/Application.h>
#include <UI/Qt/BrowserWindow.h>
#include <UI/Qt/ChromeLayout.h>
#include <UI/Qt/ChromeStyle.h>
#include <UI/Qt/Icon.h>
#if defined(AK_OS_MACOS)
# include <UI/Qt/MacWindow.h>
#endif
#include <UI/Qt/Menu.h>
#include <UI/Qt/Settings.h>
#include <UI/Qt/Tab.h>
@ -48,28 +52,32 @@
namespace Ladybird {
static constexpr auto LADYBIRD_TAB_MIME_TYPE = "application/x-ladybird-tab";
static constexpr int HORIZONTAL_TAB_STRIP_HEIGHT = 43;
static constexpr int HORIZONTAL_TAB_HEIGHT = 36;
static constexpr int HORIZONTAL_TAB_STRIP_HEIGHT = 44;
static constexpr int HORIZONTAL_TAB_HEIGHT = 38;
static constexpr int HORIZONTAL_TAB_MIN_WIDTH = 128;
static constexpr int HORIZONTAL_TAB_MAX_WIDTH = 240;
static constexpr int VERTICAL_TAB_HEIGHT = 38;
static constexpr int VERTICAL_TABS_COLLAPSED_WIDTH = 52;
static constexpr int VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH = 232;
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;
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_COLLAPSED_SIDE_MARGIN = 6;
static constexpr int VERTICAL_TABS_EXPANDED_SIDE_MARGIN = 5;
static constexpr int VERTICAL_TABS_NEW_TAB_SEPARATOR_SPACING = 4;
static constexpr int VERTICAL_TAB_SHAPE_HORIZONTAL_INSET = 5;
static constexpr int VERTICAL_TABS_TOP_MARGIN = 8;
static constexpr int VERTICAL_TABS_MACOS_TRAFFIC_LIGHTS_LEFT_MARGIN = 12;
static constexpr int TAB_CARD_SHAPE_HORIZONTAL_INSET = 5;
static constexpr int TAB_CARD_SHAPE_VERTICAL_INSET = 3;
static constexpr int VERTICAL_TAB_CONTENT_HORIZONTAL_INSET = 8;
static constexpr int VERTICAL_TABS_HOVER_COLLAPSE_POLL_INTERVAL_MS = 250;
static constexpr int TAB_BUTTON_SIZE = 22;
static constexpr int TAB_ICON_SIZE = 16;
static constexpr int COLLAPSED_VERTICAL_TAB_BUTTON_SIZE = 16;
static constexpr int COLLAPSED_VERTICAL_TAB_ICON_SIZE = 12;
static constexpr auto VERTICAL_TABS_EXPANDED_PROPERTY = "verticalTabsExpanded";
static constexpr auto COLLAPSED_VERTICAL_TAB_BUTTON_PROPERTY = "collapsedVerticalTabButton";
static constexpr auto FULL_WIDTH_TOOLBAR_PROPERTY = "fullWidthToolbar";
static constexpr auto VERTICAL_TABS_EXPANDED_PROPERTY = "verticalTabsExpanded";
static constexpr auto VERTICAL_TABS_BUTTON_PROPERTY = "verticalTabsButton";
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_HOVERED_PROPERTY = "hovered";
static constexpr auto VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY = "active";
@ -108,12 +116,27 @@ static constexpr int vertical_tabs_side_margin(bool expanded)
return expanded ? VERTICAL_TABS_EXPANDED_SIDE_MARGIN : VERTICAL_TABS_COLLAPSED_SIDE_MARGIN;
}
static constexpr int vertical_tabs_left_margin(TabLayout tab_layout)
{
return vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed);
}
static constexpr int vertical_tabs_right_margin(TabLayout tab_layout)
{
return vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed);
}
static constexpr int vertical_tabs_horizontal_margin_width(TabLayout tab_layout)
{
return vertical_tabs_left_margin(tab_layout) + vertical_tabs_right_margin(tab_layout);
}
static constexpr int vertical_tab_width(int available_width, TabLayout tab_layout)
{
if (available_width > 0)
return available_width;
auto width = tab_layout == TabLayout::VerticalCollapsed ? VERTICAL_TABS_COLLAPSED_WIDTH : VERTICAL_TABS_DEFAULT_EXPANDED_WIDTH;
return width - (vertical_tabs_side_margin(tab_layout != TabLayout::VerticalCollapsed) * 2);
return width - vertical_tabs_horizontal_margin_width(tab_layout);
}
static constexpr int clamp_vertical_tabs_expanded_width(int width)
@ -121,14 +144,46 @@ static constexpr int clamp_vertical_tabs_expanded_width(int width)
return clamp(width, VERTICAL_TABS_MIN_EXPANDED_WIDTH, VERTICAL_TABS_MAX_EXPANDED_WIDTH);
}
static QRectF expanded_vertical_tab_shape_rect(QRectF const& rect)
static QRectF tab_card_shape_rect(QRectF const& rect)
{
return rect.adjusted(VERTICAL_TAB_SHAPE_HORIZONTAL_INSET, 3.0, -VERTICAL_TAB_SHAPE_HORIZONTAL_INSET, -3.0);
return rect.adjusted(TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
}
static QRect expanded_vertical_tab_shape_rect(QRect const& rect)
static QRectF horizontal_tab_card_shape_rect(QRectF const& rect)
{
return rect.adjusted(VERTICAL_TAB_SHAPE_HORIZONTAL_INSET, 3, -VERTICAL_TAB_SHAPE_HORIZONTAL_INSET, -3);
return rect.adjusted(TAB_CARD_SHAPE_HORIZONTAL_INSET, TAB_CARD_SHAPE_VERTICAL_INSET, -TAB_CARD_SHAPE_HORIZONTAL_INSET, -TAB_CARD_SHAPE_VERTICAL_INSET);
}
static QRect tab_card_shape_rect(QRect 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);
}
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;
}
static QRectF collapsed_vertical_tab_shape_rect(QRectF const& rect)
@ -152,17 +207,20 @@ public:
private:
virtual void paintEvent(QPaintEvent* event) override
{
if (!property(VERTICAL_TABS_EXPANDED_PROPERTY).toBool()) {
if (!property(VERTICAL_TABS_BUTTON_PROPERTY).toBool()) {
QToolButton::paintEvent(event);
return;
}
auto expanded = property(VERTICAL_TABS_EXPANDED_PROPERTY).toBool();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
auto dark = ChromeStyle::is_dark(palette());
auto surface = ChromeStyle::chrome_surface(palette());
auto shape_rect = expanded_vertical_tab_shape_rect(QRectF(rect()));
auto shape_rect = expanded
? tab_card_shape_rect(QRectF(rect()))
: QRectF(rect()).adjusted(4.0, 3.0, -4.0, -3.0);
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
if (isDown()) {
@ -170,22 +228,25 @@ private:
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.setBrush(tab_hover_surface(palette(), 1.0));
painter.setPen(Qt::NoPen);
painter.drawPath(tab_path);
}
auto contents_rect = shape_rect.toAlignedRect().adjusted(VERTICAL_TAB_CONTENT_HORIZONTAL_INSET, 0, -VERTICAL_TAB_CONTENT_HORIZONTAL_INSET, 0);
auto icon_size = iconSize();
auto icon_size = QSize(TAB_ICON_SIZE, TAB_ICON_SIZE);
QRect icon_rect {
contents_rect.left() - ((icon_size.width() - TAB_ICON_SIZE) / 2),
expanded ? contents_rect.left() - ((icon_size.width() - TAB_ICON_SIZE) / 2) : rect().center().x() - (icon_size.width() / 2),
contents_rect.top() + ((contents_rect.height() - icon_size.height()) / 2),
icon_size.width(),
icon_size.height(),
};
if (!expanded)
painter.translate(1.0, 0);
icon().paint(&painter, icon_rect);
if (!expanded)
return;
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);
@ -249,7 +310,7 @@ void TabBar::set_tab_layout(TabLayout tab_layout)
if (m_tab_layout == TabLayout::Horizontal) {
setShape(QTabBar::RoundedNorth);
setMinimumSize({ 0, 42 });
setMinimumSize({ 0, HORIZONTAL_TAB_HEIGHT });
setMaximumWidth(QWIDGETSIZE_MAX);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
setUsesScrollButtons(true);
@ -337,7 +398,6 @@ void TabBar::paintEvent(QPaintEvent*)
auto border = ChromeStyle::chrome_border(palette());
auto dark = ChromeStyle::is_dark(palette());
auto text_color = ChromeStyle::chrome_text(palette());
auto is_vertical = tab_layout() != TabLayout::Horizontal;
auto is_collapsed_vertical = tab_layout() == TabLayout::VerticalCollapsed;
@ -358,46 +418,34 @@ void TabBar::paintEvent(QPaintEvent*)
QRectF shape_rect;
if (is_collapsed_vertical)
shape_rect = collapsed_vertical_tab_shape_rect(QRectF(tab_rect));
else if (is_vertical)
shape_rect = expanded_vertical_tab_shape_rect(QRectF(tab_rect));
else if (m_tab_layout == TabLayout::Horizontal)
shape_rect = horizontal_tab_card_shape_rect(QRectF(tab_rect));
else
shape_rect = QRectF(tab_rect).adjusted(3.0, 2.0, -3.0, 1.0);
auto tab_path = tab_shape_path(shape_rect, 9.0, is_vertical ? 9.0 : 8.0);
auto surface = ChromeStyle::chrome_surface(palette());
shape_rect = tab_card_shape_rect(QRectF(tab_rect));
auto tab_path = tab_shape_path(shape_rect, 9.0, 9.0);
if (is_selected) {
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));
auto selected_gradient = QLinearGradient(shape_rect.topLeft(), shape_rect.bottomLeft());
selected_gradient.setColorAt(0.0, ChromeStyle::chrome_active_tab_surface_top(palette()));
selected_gradient.setColorAt(1.0, ChromeStyle::chrome_active_tab_surface_bottom(palette()));
auto active_border = border;
active_border.setAlpha(dark ? 62 : 72);
painter.setBrush(selected_gradient);
painter.setPen(QPen(active_border, 1));
painter.setPen(QPen(selected_tab_border(palette(), is_collapsed_vertical), 1));
painter.drawPath(tab_path);
} else if (is_hovered) {
auto hover = dark ? ChromeStyle::chrome_surface_hover(palette()) : ChromeStyle::mix(surface, ChromeStyle::chrome_surface_hover(palette()), 0.28);
hover.setAlpha(static_cast<int>((dark ? 120 : 136) * hover_progress));
auto hover_border = border;
hover_border.setAlpha(static_cast<int>((dark ? 58 : 64) * hover_progress));
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);
auto inactive_border = border;
inactive_border.setAlpha(dark ? 38 : 26);
painter.setBrush(inactive);
painter.setPen(QPen(inactive_border, 1));
painter.setBrush(tab_hover_surface(palette(), hover_progress));
painter.setPen(Qt::NoPen);
painter.drawPath(tab_path);
}
if (!is_selected && !is_hovered && index > 0 && index != currentIndex() + 1 && !is_collapsed_vertical) {
auto separator = border;
separator.setAlpha(dark ? 42 : 36);
separator.setAlpha(dark ? 24 : 20);
painter.setPen(separator);
if (is_vertical)
painter.drawLine(QPoint(tab_rect.left() + 16, tab_rect.top()), QPoint(tab_rect.right() - 16, tab_rect.top()));
@ -416,7 +464,10 @@ void TabBar::paintEvent(QPaintEvent*)
TAB_ICON_SIZE,
TAB_ICON_SIZE,
};
if (!is_selected)
painter.setOpacity(is_hovered ? 0.92 : (dark ? 0.88 : 0.84));
icon.paint(&painter, icon_rect);
painter.setOpacity(1.0);
continue;
}
@ -433,7 +484,10 @@ void TabBar::paintEvent(QPaintEvent*)
TAB_ICON_SIZE,
TAB_ICON_SIZE,
};
if (!is_selected)
painter.setOpacity(is_hovered ? 0.92 : (dark ? 0.88 : 0.84));
icon.paint(&painter, icon_rect);
painter.setOpacity(1.0);
contents_rect.setLeft(icon_rect.right() + (is_vertical ? 8 : 7));
}
@ -444,7 +498,10 @@ void TabBar::paintEvent(QPaintEvent*)
QFontMetrics font_metrics(tab_font);
auto title = font_metrics.elidedText(tabText(index), Qt::ElideRight, max(0, contents_rect.width()));
painter.setPen(text_color);
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);
painter.drawText(contents_rect, Qt::AlignLeft | Qt::AlignVCenter, title);
}
@ -575,6 +632,10 @@ void TabBar::mousePressEvent(QMouseEvent* event)
return;
}
} else {
if (pressed_tab < 0 && event->button() == Qt::LeftButton && start_window_move()) {
event->accept();
return;
}
QTabBar::mousePressEvent(event);
}
@ -586,7 +647,8 @@ void TabBar::mousePressEvent(QMouseEvent* event)
void TabBar::mouseMoveEvent(QMouseEvent* event)
{
set_hovered_tab_index(tab_index_at(event->pos()));
auto hovered_tab = tab_index_at(event->pos());
set_hovered_tab_index(hovered_tab);
if (count() == 0) {
if (tab_layout() == TabLayout::Horizontal)
@ -980,7 +1042,7 @@ void TabBar::update_tab_button_geometry()
place_collapsed_button(index, QTabBar::LeftSide, tab_rect, shape_rect);
place_collapsed_button(index, QTabBar::RightSide, tab_rect, shape_rect);
} else {
auto shape_rect = expanded_vertical_tab_shape_rect(tab_rect);
auto shape_rect = tab_card_shape_rect(tab_rect);
place_expanded_button(index, QTabBar::LeftSide, shape_rect);
place_expanded_button(index, QTabBar::RightSide, shape_rect);
}
@ -1001,6 +1063,10 @@ bool TabBar::start_window_move()
auto* handle = window()->windowHandle();
if (!handle)
return false;
#if defined(AK_OS_MACOS)
if (start_appkit_window_drag(*this))
return true;
#endif
return handle->startSystemMove();
}
@ -1025,6 +1091,8 @@ TabWidget::TabWidget(QWidget* parent)
m_tab_bar->setDrawBase(false);
m_tab_bar->installEventFilter(this);
m_toolbar_container = new QStackedWidget(this);
m_toolbar_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
m_stacked_widget = new QStackedWidget(this);
m_new_tab_button = new NewTabButton(*m_tab_bar, this);
@ -1035,9 +1103,11 @@ TabWidget::TabWidget(QWidget* parent)
m_new_tab_button->setToolTip("New Tab");
m_new_tab_button->installEventFilter(this);
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);
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;
m_vertical_tabs_new_tab_separator = new QWidget(this);
m_vertical_tabs_new_tab_separator->setObjectName("LadybirdVerticalTabsSeparator");
@ -1058,18 +1128,25 @@ TabWidget::TabWidget(QWidget* parent)
m_vertical_tabs_content_layout->setContentsMargins(0, 0, 0, 0);
m_vertical_tabs_content->installEventFilter(this);
m_vertical_tab_bar_column = new QWidget(m_vertical_tabs_content);
m_vertical_tab_bar_column = new QWidget(this);
m_vertical_tab_bar_column->setObjectName("LadybirdVerticalTabBar");
m_vertical_tab_bar_column->setAttribute(Qt::WA_NativeWindow);
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_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();
m_vertical_tabs_reserved_space = new QWidget(m_vertical_tabs_content);
m_vertical_tabs_reserved_space->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
m_vertical_tabs_resize_handle = new QWidget(m_vertical_tabs_content);
m_vertical_tabs_resize_handle = new QWidget(this);
m_vertical_tabs_resize_handle->setObjectName("LadybirdVerticalTabsResizeHandle");
m_vertical_tabs_resize_handle->setAttribute(Qt::WA_NativeWindow);
m_vertical_tabs_resize_handle->setCursor(Qt::SizeHorCursor);
m_vertical_tabs_resize_handle->installEventFilter(this);
m_vertical_tabs_resize_handle->hide();
@ -1091,6 +1168,9 @@ TabWidget::TabWidget(QWidget* parent)
m_stacked_widget->setCurrentIndex(index);
emit current_tab_changed(index);
m_toolbar_container->setCurrentIndex(index);
update_vertical_tabs_overlay_geometry();
update_vertical_tabs_resize_handle();
});
connect(m_tab_bar, &QTabBar::tabCloseRequested, this, &TabWidget::tab_close_requested);
@ -1102,6 +1182,12 @@ TabWidget::TabWidget(QWidget* parent)
auto* widget = m_stacked_widget->widget(from);
m_stacked_widget->removeWidget(widget);
m_stacked_widget->insertWidget(to, widget);
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);
}
m_stacked_widget->setCurrentIndex(m_tab_bar->currentIndex());
});
@ -1125,10 +1211,11 @@ void TabWidget::insert_tab(int index, Tab* widget, QString const& label)
{
m_stacked_widget->insertWidget(index, widget);
m_tab_bar->insertTab(index, label);
m_toolbar_container->insertWidget(index, widget->toolbar_container());
widget->set_toolbar_window_controls_visible(m_window_controls_visible && m_vertical_tabs_enabled);
widget->set_vertical_tabs_enabled(m_vertical_tabs_enabled);
update_toolbar_placement();
update_tab_layout();
update_tab_button_visibility();
}
@ -1145,6 +1232,10 @@ Tab* TabWidget::take_tab(int index)
return nullptr;
m_stacked_widget->removeWidget(widget);
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);
m_tab_bar->removeTab(index);
@ -1189,8 +1280,7 @@ void TabWidget::set_tab_bar_visible(bool visible)
void TabWidget::set_window_controls_visible(bool visible)
{
m_window_controls_visible = visible;
for (int index = 0; index < m_stacked_widget->count(); ++index)
tab(index)->set_toolbar_window_controls_visible(visible && m_vertical_tabs_enabled);
update_tab_toolbar_window_controls_visibility();
update_tab_chrome_visibility();
update_tab_layout();
}
@ -1204,7 +1294,6 @@ void TabWidget::set_vertical_tabs_enabled(bool enabled)
if (!m_vertical_tabs_enabled)
set_vertical_tabs_hover_expanded(false);
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_vertical_tabs_enabled(enabled);
}
rebuild_layout();
@ -1263,11 +1352,13 @@ void TabWidget::dropEvent(QDropEvent* event)
bool TabWidget::eventFilter(QObject* watched, QEvent* event)
{
if (watched == window() && event->type() == QEvent::Leave)
set_vertical_tabs_hover_expanded(false);
defer_update_vertical_tabs_hover_expanded();
if (watched == m_vertical_tabs_resize_handle) {
auto reset_resize_handle = [this] {
m_is_resizing_vertical_tabs = false;
m_vertical_tabs_resize_handle->releaseMouse();
QApplication::restoreOverrideCursor();
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, false);
};
@ -1291,6 +1382,8 @@ bool TabWidget::eventFilter(QObject* watched, QEvent* 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;
m_vertical_tabs_resize_handle->grabMouse();
QApplication::setOverrideCursor(Qt::SizeHorCursor);
set_resize_handle_property(VERTICAL_TABS_RESIZE_HANDLE_ACTIVE_PROPERTY, true);
return true;
} else if (event->type() == QEvent::MouseMove) {
@ -1428,17 +1521,14 @@ bool TabWidget::can_expand_vertical_tabs_on_hover() const
bool TabWidget::cursor_is_over_vertical_tabs() const
{
if (!m_vertical_tab_bar_column->isVisible())
if (!m_vertical_tabs_content->isVisible())
return false;
auto cursor_position = m_vertical_tab_bar_column->mapFromGlobal(QCursor::pos());
if (m_vertical_tab_bar_column->rect().contains(cursor_position))
return true;
return m_vertical_tab_bar_column->underMouse()
|| m_tab_bar->underMouse()
|| m_new_tab_button->underMouse()
|| m_vertical_tabs_new_tab_separator->underMouse();
auto vertical_tabs_rect = QRect {
m_vertical_tabs_content->mapToGlobal(QPoint { 0, 0 }),
QSize { current_vertical_tabs_width(), m_vertical_tabs_content->height() }
};
return vertical_tabs_rect.contains(QCursor::pos());
}
int TabWidget::vertical_tabs_layout_width() const
@ -1446,6 +1536,59 @@ int TabWidget::vertical_tabs_layout_width() const
return m_vertical_tabs_expanded ? m_vertical_tabs_expanded_width : VERTICAL_TABS_COLLAPSED_WIDTH;
}
bool TabWidget::uses_full_width_toolbar_for_current_layout() const
{
return browser_chrome_layout_policy().controls_placement == WindowControlsPlacement::LeftTrafficLights
&& m_vertical_tabs_enabled
&& current_tab_layout() != TabLayout::Horizontal;
}
bool TabWidget::should_show_window_controls_in_tab_toolbar() const
{
if (!m_window_controls_visible)
return false;
if (browser_chrome_layout_policy().controls_placement == WindowControlsPlacement::RightCustomControls)
return m_tab_bar->tab_layout() != TabLayout::Horizontal;
return uses_full_width_toolbar_for_current_layout();
}
void TabWidget::update_toolbar_placement()
{
auto use_full_width_toolbar = uses_full_width_toolbar_for_current_layout();
for (int index = 0; index < m_stacked_widget->count(); ++index) {
auto* current_tab = tab(index);
auto* toolbar = current_tab->toolbar_container();
toolbar->setProperty(FULL_WIDTH_TOOLBAR_PROPERTY, use_full_width_toolbar);
toolbar->style()->unpolish(toolbar);
toolbar->style()->polish(toolbar);
toolbar->update();
if (use_full_width_toolbar) {
current_tab->set_toolbar_container_in_tab_layout(false);
if (m_toolbar_container->indexOf(toolbar) == -1)
m_toolbar_container->insertWidget(index, toolbar);
} else {
if (m_toolbar_container->indexOf(toolbar) != -1)
m_toolbar_container->removeWidget(toolbar);
current_tab->set_toolbar_container_in_tab_layout(true);
}
}
m_toolbar_container->setVisible(use_full_width_toolbar);
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);
}
void TabWidget::rebuild_layout()
{
clear_layout(*m_main_layout);
@ -1454,10 +1597,13 @@ void TabWidget::rebuild_layout()
clear_layout(*m_vertical_tabs_content_layout);
m_tab_bar->set_tab_layout(current_tab_layout());
update_toolbar_placement();
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
rebuild_layout_for_vertical_tabs();
if (uses_full_width_toolbar_for_current_layout())
m_main_layout->addWidget(m_toolbar_container);
m_vertical_tabs_content_layout->addWidget(m_vertical_tabs_reserved_space);
m_vertical_tabs_content_layout->addWidget(m_stacked_widget, 1);
m_main_layout->addWidget(m_vertical_tabs_content, 1);
@ -1483,54 +1629,75 @@ void TabWidget::rebuild_layout_for_horizontal_tabs()
m_new_tab_button->setText({});
m_new_tab_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, false);
m_new_tab_button->setProperty(VERTICAL_TABS_BUTTON_PROPERTY, false);
m_new_tab_button->style()->unpolish(m_new_tab_button);
m_new_tab_button->style()->polish(m_new_tab_button);
m_new_tab_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_new_tab_button->setFixedSize(32, 32);
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
if (use_left_traffic_light_window_controls()) {
m_tab_bar_row_layout->addWidget(m_window_controls, 0, Qt::AlignVCenter);
m_tab_bar_row_layout->addSpacing(8);
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;
}
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);
m_tab_bar_row_layout->addWidget(m_minimize_window_button);
m_tab_bar_row_layout->addWidget(m_maximize_window_button);
m_tab_bar_row_layout->addWidget(m_close_window_button);
m_tab_bar_row_layout->addWidget(m_window_controls);
}
void TabWidget::rebuild_layout_for_vertical_tabs()
{
auto expanded = vertical_tabs_effectively_expanded();
auto reserved_width = vertical_tabs_layout_width();
auto const chrome_layout_policy = browser_chrome_layout_policy();
auto side_bar_width = current_vertical_tabs_width();
m_vertical_tabs_reserved_space->setFixedWidth(reserved_width);
m_vertical_tab_bar_column->setFixedWidth(side_bar_width);
m_vertical_tab_bar_column_layout->setSpacing(expanded ? 0 : VERTICAL_TABS_NEW_TAB_SEPARATOR_SPACING);
auto side_margin = vertical_tabs_side_margin(expanded);
m_vertical_tab_bar_column_layout->setContentsMargins(side_margin, 8, side_margin, 8);
m_vertical_tab_bar_column_layout->setSpacing(0);
auto tab_layout = m_tab_bar->tab_layout();
auto left_margin = vertical_tabs_left_margin(tab_layout);
auto right_margin = vertical_tabs_right_margin(tab_layout);
auto tab_width = max(0, side_bar_width - vertical_tabs_horizontal_margin_width(tab_layout));
auto top_margin = VERTICAL_TABS_TOP_MARGIN;
auto window_controls_bottom_spacing = 0;
auto show_sidebar_traffic_lights = chrome_layout_policy.controls_placement == WindowControlsPlacement::LeftTrafficLights
&& m_vertical_tabs_expanded
&& !uses_full_width_toolbar_for_current_layout();
if (show_sidebar_traffic_lights) {
auto window_controls_height = WindowControlButton::default_size({ 40, 40 }).height();
top_margin = max(0, (chrome_layout_policy.toolbar_height - window_controls_height) / 2);
window_controls_bottom_spacing = max(0, chrome_layout_policy.toolbar_height - window_controls_height - top_margin) + VERTICAL_TABS_TOP_MARGIN;
auto window_controls_left_padding = max(0, VERTICAL_TABS_MACOS_TRAFFIC_LIGHTS_LEFT_MARGIN - left_margin);
m_window_controls->layout()->setContentsMargins(window_controls_left_padding, 0, 0, 0);
} else {
m_window_controls->layout()->setContentsMargins(0, 0, 0, 0);
}
m_vertical_tab_bar_column_layout->setContentsMargins(left_margin, top_margin, right_margin, 8);
if (show_sidebar_traffic_lights) {
m_vertical_tab_bar_column_layout->addWidget(m_window_controls, 0, Qt::AlignLeft);
if (window_controls_bottom_spacing > 0)
m_vertical_tab_bar_column_layout->addSpacing(window_controls_bottom_spacing);
}
m_new_tab_button->setToolButtonStyle(expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly);
update_vertical_tabs_action_labels();
m_new_tab_button->setProperty(VERTICAL_TABS_BUTTON_PROPERTY, true);
m_new_tab_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, expanded);
m_vertical_tabs_new_tab_separator->setVisible(true);
if (expanded) {
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);
} else {
m_new_tab_button->setFixedSize(32, VERTICAL_TAB_HEIGHT);
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
m_new_tab_button->style()->unpolish(m_new_tab_button);
m_new_tab_button->style()->polish(m_new_tab_button);
m_vertical_tabs_new_tab_separator->hide();
update_vertical_new_tab_button_geometry(tab_width);
Qt::Alignment new_tab_button_alignment {};
if (!expanded)
new_tab_button_alignment = Qt::AlignHCenter;
m_vertical_tab_bar_column_layout->addWidget(m_tab_bar);
if (expanded)
m_vertical_tab_bar_column_layout->addSpacing(VERTICAL_TABS_NEW_TAB_SEPARATOR_SPACING);
m_vertical_tab_bar_column_layout->addWidget(m_vertical_tabs_new_tab_separator);
if (expanded)
m_vertical_tab_bar_column_layout->addSpacing(VERTICAL_TABS_NEW_TAB_SEPARATOR_SPACING);
m_vertical_tab_bar_column_layout->addWidget(m_new_tab_button, 0, new_tab_button_alignment);
m_vertical_tab_bar_column_layout->addWidget(m_new_tab_button);
m_vertical_tab_bar_column_layout->addStretch(1);
}
@ -1571,6 +1738,9 @@ void TabWidget::update_vertical_tabs_resize_handle()
auto show_resize_handle = is_vertical && m_vertical_tabs_expanded;
m_vertical_tabs_resize_handle->setVisible(show_resize_handle);
if (!show_resize_handle) {
m_vertical_tabs_resize_handle->releaseMouse();
if (m_is_resizing_vertical_tabs)
QApplication::restoreOverrideCursor();
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);
@ -1579,38 +1749,80 @@ void TabWidget::update_vertical_tabs_resize_handle()
auto handle_width = VERTICAL_TABS_RESIZE_HIT_AREA_WIDTH;
auto divider_x = vertical_tabs_layout_width() - 1;
m_vertical_tabs_resize_handle->setGeometry(divider_x - (handle_width / 2), 0, handle_width, m_vertical_tabs_content->height());
m_vertical_tabs_resize_handle->setGeometry(divider_x - (handle_width / 2), m_vertical_tabs_content->y(), handle_width, m_vertical_tabs_content->height());
m_vertical_tabs_resize_handle->raise();
}
void TabWidget::update_vertical_tabs_content_separator()
{
auto show_content_separator = m_tab_bar_visible && uses_full_width_toolbar_for_current_layout();
m_vertical_tabs_content_separator->setVisible(show_content_separator);
if (!show_content_separator)
return;
auto separator_x = max(0, current_vertical_tabs_width() - 1);
m_vertical_tabs_content_separator->setGeometry(separator_x, browser_chrome_layout_policy().toolbar_height - 1, max(0, width() - separator_x), 1);
m_vertical_tabs_content_separator->raise();
}
void TabWidget::update_vertical_tabs_action_labels()
{
m_new_tab_button->setText(vertical_tabs_effectively_expanded() ? "New Tab" : QString {});
}
void TabWidget::update_vertical_tabs_hover_layout()
{
m_tab_bar->set_tab_layout(current_tab_layout());
auto tab_layout = m_tab_bar->tab_layout();
auto left_margin = vertical_tabs_left_margin(tab_layout);
auto right_margin = vertical_tabs_right_margin(tab_layout);
auto side_bar_width = current_vertical_tabs_width();
auto tab_width = max(0, side_bar_width - vertical_tabs_horizontal_margin_width(tab_layout));
auto expanded = vertical_tabs_effectively_expanded();
m_vertical_tab_bar_column_layout->setContentsMargins(left_margin, VERTICAL_TABS_TOP_MARGIN, right_margin, 8);
m_new_tab_button->setToolButtonStyle(expanded ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly);
update_vertical_tabs_action_labels();
m_new_tab_button->setProperty(VERTICAL_TABS_BUTTON_PROPERTY, true);
m_new_tab_button->setProperty(VERTICAL_TABS_EXPANDED_PROPERTY, expanded);
m_new_tab_button->style()->unpolish(m_new_tab_button);
m_new_tab_button->style()->polish(m_new_tab_button);
update_vertical_new_tab_button_geometry(tab_width);
update_tab_button_visibility();
update_tab_layout();
}
void TabWidget::update_vertical_new_tab_button_geometry(int tab_width)
{
m_new_tab_button->setFixedSize(tab_width, VERTICAL_TAB_HEIGHT);
m_new_tab_button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
void TabWidget::update_tab_layout()
{
if (m_tab_bar->tab_layout() != TabLayout::Horizontal) {
auto expanded = vertical_tabs_effectively_expanded();
auto reserved_width = vertical_tabs_layout_width();
auto side_bar_width = current_vertical_tabs_width();
auto tab_width = max(0, side_bar_width - vertical_tabs_horizontal_margin_width(m_tab_bar->tab_layout()));
m_vertical_tabs_reserved_space->setFixedWidth(reserved_width);
m_vertical_tab_bar_column->setFixedWidth(side_bar_width);
update_vertical_new_tab_button_geometry(tab_width);
update_vertical_tabs_overlay_geometry();
m_tab_bar->set_available_width(side_bar_width - (vertical_tabs_side_margin(expanded) * 2));
m_vertical_tabs_content_layout->activate();
m_tab_bar->set_available_width(tab_width);
update_vertical_tabs_resize_handle();
update_vertical_tabs_content_separator();
return;
}
update_vertical_tabs_resize_handle();
update_vertical_tabs_content_separator();
auto controls_width = m_new_tab_button->width();
if (m_minimize_window_button->isVisible())
controls_width += m_minimize_window_button->width();
if (m_maximize_window_button->isVisible())
controls_width += m_maximize_window_button->width();
if (m_close_window_button->isVisible())
controls_width += m_close_window_button->width();
if (m_window_controls->isVisible())
controls_width += m_window_controls->width();
auto available_for_tabs = width() - controls_width - 36;
@ -1644,13 +1856,17 @@ void TabWidget::update_tab_button_visibility()
void TabWidget::update_tab_chrome_visibility()
{
auto show_top_row = m_tab_bar_visible && m_tab_bar->tab_layout() == TabLayout::Horizontal;
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;
m_tab_bar_row->setVisible(show_top_row);
m_vertical_tab_bar_column->setVisible(m_tab_bar_visible && m_tab_bar->tab_layout() != TabLayout::Horizontal);
auto show_tab_strip_window_controls = m_window_controls_visible && m_tab_bar->tab_layout() == TabLayout::Horizontal;
m_minimize_window_button->setVisible(show_tab_strip_window_controls);
m_maximize_window_button->setVisible(show_tab_strip_window_controls);
m_close_window_button->setVisible(show_tab_strip_window_controls);
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
&& (is_horizontal || (browser_chrome_layout_policy().controls_placement == WindowControlsPlacement::LeftTrafficLights && m_vertical_tabs_expanded && !uses_full_width_toolbar_for_current_layout()));
m_window_controls->setVisible(show_window_controls);
}
void TabWidget::recreate_icons()
@ -1670,16 +1886,20 @@ 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_content_separator->setStyleSheet(style_sheet);
m_vertical_tabs_resize_handle->setStyleSheet(style_sheet);
m_is_updating_chrome_style = false;
}
void TabWidget::update_vertical_tabs_overlay_geometry()
{
if (m_tab_bar->tab_layout() == TabLayout::Horizontal)
if (m_tab_bar->tab_layout() == TabLayout::Horizontal) {
m_vertical_tab_bar_column->hide();
return;
}
m_vertical_tab_bar_column->setGeometry(0, 0, current_vertical_tabs_width(), m_vertical_tabs_content->height());
m_vertical_tab_bar_column->setGeometry(0, m_vertical_tabs_content->y(), current_vertical_tabs_width(), m_vertical_tabs_content->height());
m_vertical_tab_bar_column->show();
m_vertical_tab_bar_column->raise();
}
@ -1694,7 +1914,7 @@ void TabWidget::set_vertical_tabs_hover_expanded(bool expanded)
m_vertical_tabs_hover_collapse_timer->start();
else
m_vertical_tabs_hover_collapse_timer->stop();
rebuild_layout();
update_vertical_tabs_hover_layout();
}
void TabWidget::defer_update_vertical_tabs_hover_expanded()
@ -1740,6 +1960,10 @@ bool TabWidget::start_window_move()
auto* handle = window()->windowHandle();
if (!handle)
return false;
#if defined(AK_OS_MACOS)
if (start_appkit_window_drag(*this))
return true;
#endif
return handle->startSystemMove();
}

View file

@ -172,16 +172,23 @@ private:
bool can_expand_vertical_tabs_on_hover() const;
bool cursor_is_over_vertical_tabs() const;
int vertical_tabs_layout_width() const;
bool uses_full_width_toolbar_for_current_layout() const;
bool should_show_window_controls_in_tab_toolbar() const;
void rebuild_layout();
void rebuild_layout_for_horizontal_tabs();
void rebuild_layout_for_vertical_tabs();
void update_toolbar_placement();
void update_tab_toolbar_window_controls_visibility();
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 update_vertical_tabs_content_separator();
void set_resize_handle_property(char const* property, bool enabled);
void update_vertical_tabs_action_labels();
void update_vertical_tabs_hover_layout();
void update_vertical_new_tab_button_geometry(int tab_width);
void update_tab_layout();
void update_tab_chrome_visibility();
void recreate_icons();
@ -201,10 +208,13 @@ private:
QToolButton* m_minimize_window_button { nullptr };
QToolButton* m_maximize_window_button { nullptr };
QToolButton* m_close_window_button { nullptr };
QWidget* m_window_controls { nullptr };
QStackedWidget* m_toolbar_container { nullptr };
QWidget* m_tab_bar_row { nullptr };
QWidget* m_vertical_tabs_new_tab_separator { nullptr };
QWidget* m_vertical_tabs_reserved_space { nullptr };
QWidget* m_vertical_tab_bar_column { nullptr };
QWidget* m_vertical_tabs_content_separator { nullptr };
QWidget* m_vertical_tabs_resize_handle { nullptr };
QWidget* m_vertical_tabs_content { nullptr };
QTimer* m_vertical_tabs_hover_collapse_timer { nullptr };

View file

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

View file

@ -14,14 +14,26 @@
class QEnterEvent;
class QEvent;
class QMouseEvent;
class QPaintEvent;
class QWidget;
namespace Ladybird {
enum class WindowControlButtonType {
Minimize,
Maximize,
Close,
};
class WindowControlButton final : public QToolButton {
public:
WindowControlButton(char const* object_name, QString const& tool_tip, QSize icon_size, QSize button_size, QWidget* parent = nullptr);
WindowControlButton(WindowControlButtonType, QString const& tool_tip, QSize icon_size, QSize button_size, QWidget* parent = nullptr);
static QSize default_size(QSize non_macos_size);
static int default_spacing();
private:
virtual void paintEvent(QPaintEvent*) override;
virtual void enterEvent(QEnterEvent*) override;
virtual void leaveEvent(QEvent*) override;
virtual void mouseMoveEvent(QMouseEvent*) override;
@ -33,7 +45,17 @@ private:
void set_pressed_outside(bool);
void update_pressed_position(QPoint const&);
[[maybe_unused]] WindowControlButtonType m_type {};
bool m_tracking_press { false };
};
struct WindowControlButtons {
QWidget* container { nullptr };
WindowControlButton* minimize { nullptr };
WindowControlButton* maximize { nullptr };
WindowControlButton* close { nullptr };
};
WindowControlButtons create_window_control_buttons(QWidget& parent, char const* object_name, QSize icon_size, QSize non_macos_button_size);
}