diff --git a/UI/Qt/Application.cpp b/UI/Qt/Application.cpp index 93d354dcfe..203a646b58 100644 --- a/UI/Qt/Application.cpp +++ b/UI/Qt/Application.cpp @@ -9,6 +9,9 @@ #include #include #include +#if defined(AK_OS_MACOS) +# include +#endif #include #include #include @@ -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(); } diff --git a/UI/Qt/BrowserWindow.cpp b/UI/Qt/BrowserWindow.cpp index 71a7b5bcf3..c9bd8ffa89 100644 --- a/UI/Qt/BrowserWindow.cpp +++ b/UI/Qt/BrowserWindow.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -362,13 +363,15 @@ BrowserWindow::BrowserWindow(Vector 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; diff --git a/UI/Qt/ChromeLayout.h b/UI/Qt/ChromeLayout.h new file mode 100644 index 0000000000..cacc818071 --- /dev/null +++ b/UI/Qt/ChromeLayout.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +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(); +} + +} diff --git a/UI/Qt/ChromeStyle.cpp b/UI/Qt/ChromeStyle.cpp index b576a7379f..6adfdbb606 100644 --- a/UI/Qt/ChromeStyle.cpp +++ b/UI/Qt/ChromeStyle.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) # include @@ -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) diff --git a/UI/Qt/Icon.cpp b/UI/Qt/Icon.cpp index 1e69a4fe5e..a1ee1b1d31 100644 --- a/UI/Qt/Icon.cpp +++ b/UI/Qt/Icon.cpp @@ -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)); diff --git a/UI/Qt/LocationEdit.cpp b/UI/Qt/LocationEdit.cpp index 311ba06a33..48d7d10501 100644 --- a/UI/Qt/LocationEdit.cpp +++ b/UI/Qt/LocationEdit.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -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(); diff --git a/UI/Qt/MacWindow.h b/UI/Qt/MacWindow.h index 4cbd7e94d9..0a7ebcf80c 100644 --- a/UI/Qt/MacWindow.h +++ b/UI/Qt/MacWindow.h @@ -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&); } diff --git a/UI/Qt/MacWindow.mm b/UI/Qt/MacWindow.mm index dc4ec6cf07..bf7309aa5c 100644 --- a/UI/Qt/MacWindow.mm +++ b/UI/Qt/MacWindow.mm @@ -6,14 +6,122 @@ #include +#include +#include +#include +#include +#include +#include #include #import #import +#import + +@interface LadybirdWindowControlHoverTracker : NSObject +@property (nonatomic, retain) NSTrackingArea* trackingArea; +- (instancetype)initWithWidget:(QWidget*)widget callback:(void (*)(QWidget*))callback; +@end + +@implementation LadybirdWindowControlHoverTracker +{ + void (*m_callback)(QWidget*); + QPointer 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(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(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(window_widget->winId()); + if (!view) + return; + + if (auto* existing_tracker = static_cast(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(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; } } diff --git a/UI/Qt/Settings.cpp b/UI/Qt/Settings.cpp index 4d6e6aca63..9126ff0f04 100644 --- a/UI/Qt/Settings.cpp +++ b/UI/Qt/Settings.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include @@ -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); } diff --git a/UI/Qt/Tab.cpp b/UI/Qt/Tab.cpp index a3f227a6dd..724254bc48 100644 --- a/UI/Qt/Tab.cpp +++ b/UI/Qt/Tab.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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 parent_client, size_t page_index) @@ -117,7 +120,7 @@ Tab::Tab(BrowserWindow* window, RefPtr 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 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 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(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() diff --git a/UI/Qt/Tab.h b/UI/Qt/Tab.h index 2526035620..ceca6f9150 100644 --- a/UI/Qt/Tab.h +++ b/UI/Qt/Tab.h @@ -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 }; diff --git a/UI/Qt/TabBar.cpp b/UI/Qt/TabBar.cpp index e55cb5b1c9..4564ae6503 100644 --- a/UI/Qt/TabBar.cpp +++ b/UI/Qt/TabBar.cpp @@ -9,8 +9,12 @@ #include #include #include +#include #include #include +#if defined(AK_OS_MACOS) +# include +#endif #include #include #include @@ -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((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((dark ? 120 : 136) * hover_progress)); - auto hover_border = border; - hover_border.setAlpha(static_cast((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(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(widget)->toolbar_container(); + if (m_toolbar_container->indexOf(toolbar) != -1) + m_toolbar_container->removeWidget(toolbar); + as(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(); } diff --git a/UI/Qt/TabBar.h b/UI/Qt/TabBar.h index 0ba5494946..d0559cd439 100644 --- a/UI/Qt/TabBar.h +++ b/UI/Qt/TabBar.h @@ -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 }; diff --git a/UI/Qt/WindowControlButton.cpp b/UI/Qt/WindowControlButton.cpp index cbd02f6da9..45cf70c43d 100644 --- a/UI/Qt/WindowControlButton.cpp +++ b/UI/Qt/WindowControlButton.cpp @@ -6,29 +6,228 @@ #include +#include +#include +#if defined(AK_OS_MACOS) +# include +#endif + +#include +#include #include #include +#include #include +#include 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(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(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; diff --git a/UI/Qt/WindowControlButton.h b/UI/Qt/WindowControlButton.h index 327ef06180..46a17158be 100644 --- a/UI/Qt/WindowControlButton.h +++ b/UI/Qt/WindowControlButton.h @@ -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); + }