diff --git a/UI/Qt/Autocomplete.cpp b/UI/Qt/Autocomplete.cpp index 383427a63b..17ad695893 100644 --- a/UI/Qt/Autocomplete.cpp +++ b/UI/Qt/Autocomplete.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -356,13 +357,16 @@ Autocomplete::Autocomplete(QLineEdit* anchor) // position_popup() rather than made its own window, so that showing // it never causes the address bar to lose keyboard focus. m_popup = new QFrame(); + m_popup->setObjectName("LadybirdAutocompletePopup"); m_popup->setFocusPolicy(Qt::NoFocus); m_popup->setFrameShape(QFrame::StyledPanel); m_popup->setFrameShadow(QFrame::Raised); m_popup->setAutoFillBackground(true); + m_popup->setStyleSheet(ChromeStyle::autocomplete_popup_style_sheet(m_anchor->palette())); m_popup->hide(); m_list_view = new QListView(m_popup); + m_list_view->setObjectName("LadybirdAutocompleteList"); m_list_view->setFocusPolicy(Qt::NoFocus); m_list_view->setSelectionMode(QAbstractItemView::SingleSelection); m_list_view->setMouseTracking(true); @@ -426,6 +430,7 @@ void Autocomplete::show_with_suggestions(Vector return; } + m_popup->setStyleSheet(ChromeStyle::autocomplete_popup_style_sheet(m_anchor->palette())); position_popup(); if (!m_popup->isVisible()) m_popup->show(); diff --git a/UI/Qt/BookmarksBar.cpp b/UI/Qt/BookmarksBar.cpp index e320536ffb..1bb93d1bdb 100644 --- a/UI/Qt/BookmarksBar.cpp +++ b/UI/Qt/BookmarksBar.cpp @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -35,15 +37,36 @@ static void install_menu_event_filter(QObject* filter, QMenu* menu) BookmarksBar::BookmarksBar(QWidget* parent) : QToolBar(parent) { + setObjectName("LadybirdBookmarksBar"); setIconSize({ BOOKMARK_BUTTON_ICON_SIZE, BOOKMARK_BUTTON_ICON_SIZE }); setVisible(WebView::Application::settings().show_bookmarks_bar()); setMovable(false); + setFloatable(false); + update_chrome_style(); installEventFilter(this); rebuild(); } +bool BookmarksBar::event(QEvent* event) +{ + if (event->type() == QEvent::PaletteChange) + update_chrome_style(); + + return QToolBar::event(event); +} + +void BookmarksBar::update_chrome_style() +{ + if (m_is_updating_chrome_style) + return; + + m_is_updating_chrome_style = true; + setStyleSheet(ChromeStyle::bookmarks_bar_style_sheet(palette())); + m_is_updating_chrome_style = false; +} + void BookmarksBar::rebuild() { for (auto* action : actions()) { diff --git a/UI/Qt/BookmarksBar.h b/UI/Qt/BookmarksBar.h index 8f2ca6acb5..3e2738d354 100644 --- a/UI/Qt/BookmarksBar.h +++ b/UI/Qt/BookmarksBar.h @@ -28,12 +28,14 @@ public: void show_context_menu(QPoint, Optional, Optional target_folder_id); private: + virtual bool event(QEvent*) override; virtual bool eventFilter(QObject* object, QEvent* event) override; bool handle_left_mouse_click(QMouseEvent*, QObject*); bool handle_middle_mouse_click(QMouseEvent*, QObject*); bool handle_right_mouse_click(QMouseEvent*, QObject*); void extract_item_properties(QObject*); + void update_chrome_style(); QMenu& bookmarks_bar_context_menu(); QMenu& bookmark_context_menu(); @@ -46,6 +48,7 @@ private: String m_selected_bookmark_menu_item_id; QString m_selected_bookmark_menu_item_type; Optional m_selected_bookmark_menu_target_folder_id; + bool m_is_updating_chrome_style { false }; }; } diff --git a/UI/Qt/BrowserWindow.cpp b/UI/Qt/BrowserWindow.cpp index 9f970141cb..87f6517c20 100644 --- a/UI/Qt/BrowserWindow.cpp +++ b/UI/Qt/BrowserWindow.cpp @@ -22,8 +22,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -187,7 +189,9 @@ BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow { auto const& browser_options = WebView::Application::browser_options(); + setWindowFlag(Qt::FramelessWindowHint); setWindowIcon(app_icon()); + qApp->installEventFilter(this); // Listen for DPI changes m_device_pixel_ratio = devicePixelRatio(); @@ -429,6 +433,11 @@ BrowserWindow::BrowserWindow(Vector const& initial_urls, IsPopupWindow on_devtools_enabled(); } +BrowserWindow::~BrowserWindow() +{ + qApp->removeEventFilter(this); +} + void BrowserWindow::rebuild_bookmarks_menu() { repopulate_application_menu(*m_bookmarks_menu, *this, Application::the().bookmarks_menu()); @@ -819,6 +828,53 @@ bool BrowserWindow::event(QEvent* event) return QMainWindow::event(event); } +bool BrowserWindow::eventFilter(QObject* object, QEvent* event) +{ + if (event->type() != QEvent::MouseButtonPress) + return QMainWindow::eventFilter(object, event); + + if (isMaximized() || isFullScreen()) + return QMainWindow::eventFilter(object, event); + + auto* widget = as_if(object); + if (!widget || widget->window() != this) + return QMainWindow::eventFilter(object, event); + if (qobject_cast(object)) + return QMainWindow::eventFilter(object, event); + + auto* mouse_event = static_cast(event); + if (mouse_event->button() != Qt::LeftButton) + return QMainWindow::eventFilter(object, event); + + auto edges = resize_edges_for_position(widget->mapTo(this, mouse_event->position().toPoint())); + if (edges == Qt::Edges {}) + return QMainWindow::eventFilter(object, event); + + auto* handle = windowHandle(); + if (!handle || !handle->startSystemResize(edges)) + return QMainWindow::eventFilter(object, event); + + return true; +} + +Qt::Edges BrowserWindow::resize_edges_for_position(QPoint const& position) const +{ + static constexpr int resize_border_width = 6; + + Qt::Edges edges; + if (position.x() <= resize_border_width) + edges |= Qt::LeftEdge; + if (position.x() >= width() - resize_border_width) + edges |= Qt::RightEdge; + if (position.y() <= resize_border_width + && (position.x() <= resize_border_width * 2 || position.x() >= width() - resize_border_width * 2)) + edges |= Qt::TopEdge; + if (position.y() >= height() - resize_border_width) + edges |= Qt::BottomEdge; + + return edges; +} + void BrowserWindow::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); diff --git a/UI/Qt/BrowserWindow.h b/UI/Qt/BrowserWindow.h index b192e1c9f3..fc51e9698a 100644 --- a/UI/Qt/BrowserWindow.h +++ b/UI/Qt/BrowserWindow.h @@ -86,6 +86,7 @@ public: }; BrowserWindow(Vector const& initial_urls, IsPopupWindow is_popup_window = IsPopupWindow::No, Tab* parent_tab = nullptr, Optional page_index = {}); + virtual ~BrowserWindow() override; WebContentView& view() const { return m_current_tab->view(); } @@ -135,6 +136,7 @@ public slots: private: virtual bool event(QEvent*) override; + virtual bool eventFilter(QObject*, QEvent*) override; virtual void resizeEvent(QResizeEvent*) override; virtual void changeEvent(QEvent* event) override; virtual void moveEvent(QMoveEvent*) override; @@ -145,6 +147,7 @@ private: void initialize_tab(Tab*); void set_current_tab(Tab* tab); + Qt::Edges resize_edges_for_position(QPoint const&) const; template void for_each_tab(Callback&& callback) diff --git a/UI/Qt/CMakeLists.txt b/UI/Qt/CMakeLists.txt index 2ed0a38a0b..6d2c4ee566 100644 --- a/UI/Qt/CMakeLists.txt +++ b/UI/Qt/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources(ladybird PRIVATE Autocomplete.cpp BookmarksBar.cpp BrowserWindow.cpp + ChromeStyle.cpp EventLoopImplementationQt.cpp EventLoopImplementationQtEventTarget.cpp FindInPageWidget.cpp diff --git a/UI/Qt/ChromeStyle.cpp b/UI/Qt/ChromeStyle.cpp new file mode 100644 index 0000000000..1fb993119d --- /dev/null +++ b/UI/Qt/ChromeStyle.cpp @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Ladybird::ChromeStyle { + +static bool is_dark(QPalette const& palette) +{ + return palette.color(QPalette::Window).lightness() < 128; +} + +static QColor mix(QColor const& from, QColor const& to, double amount) +{ + auto channel = [&](int from_channel, int to_channel) { + return static_cast(from_channel + (to_channel - from_channel) * amount); + }; + + return QColor { + channel(from.red(), to.red()), + channel(from.green(), to.green()), + channel(from.blue(), to.blue()), + }; +} + +QColor chrome_background(QPalette const& palette) +{ + auto window = palette.color(QPalette::Window); + return is_dark(palette) + ? mix(window, QColor(18, 22, 28), 0.42) + : mix(window, QColor(240, 244, 248), 0.58); +} + +QColor chrome_surface(QPalette const& palette) +{ + auto base = palette.color(QPalette::Base); + return is_dark(palette) + ? mix(base, QColor(40, 45, 54), 0.46) + : mix(base, QColor(255, 255, 255), 0.72); +} + +QColor chrome_surface_hover(QPalette const& palette) +{ + auto accent = palette.color(QPalette::Highlight); + return is_dark(palette) + ? mix(chrome_surface(palette), accent.lighter(135), 0.18) + : mix(chrome_surface(palette), accent.lighter(165), 0.22); +} + +QColor chrome_surface_pressed(QPalette const& palette) +{ + auto accent = palette.color(QPalette::Highlight); + return is_dark(palette) + ? mix(chrome_surface(palette), accent.lighter(125), 0.28) + : mix(chrome_surface(palette), accent.lighter(145), 0.32); +} + +QColor chrome_border(QPalette const& palette) +{ + return is_dark(palette) + ? mix(chrome_surface(palette), QColor(255, 255, 255), 0.16) + : mix(chrome_background(palette), QColor(92, 105, 120), 0.22); +} + +QColor chrome_accent(QPalette const& palette) +{ + return palette.color(QPalette::Highlight); +} + +QColor chrome_muted_text(QPalette const& palette) +{ + return palette.color(QPalette::PlaceholderText); +} + +QString style_sheet_color(QColor const& color) +{ + return QStringLiteral("rgb(%1, %2, %3)").arg(color.red()).arg(color.green()).arg(color.blue()); +} + +QString navigation_toolbar_style_sheet(QPalette const& palette) +{ + auto background = style_sheet_color(chrome_background(palette)); + auto surface_hover = style_sheet_color(chrome_surface_hover(palette)); + auto surface_pressed = style_sheet_color(chrome_surface_pressed(palette)); + auto border = style_sheet_color(chrome_border(palette)); + auto text = style_sheet_color(palette.color(QPalette::ButtonText)); + auto disabled_text = style_sheet_color(chrome_muted_text(palette)); + + return QStringLiteral(R"( +QToolBar#LadybirdNavigationToolbar { + background: %1; + border: 0; + border-bottom: 1px solid %4; + padding: 5px 8px 6px 8px; + spacing: 6px; +} + +QToolBar#LadybirdNavigationToolbar QToolButton { + color: %5; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + min-width: 30px; + min-height: 30px; + padding: 0 5px; +} + +QToolBar#LadybirdNavigationToolbar QToolButton:hover { + background: %2; + border-color: %4; +} + +QToolBar#LadybirdNavigationToolbar QToolButton:pressed, +QToolBar#LadybirdNavigationToolbar QToolButton:checked { + background: %3; + border-color: %4; +} + +QToolBar#LadybirdNavigationToolbar QToolButton:disabled { + color: %6; +} + +QToolBar#LadybirdNavigationToolbar QToolButton::menu-indicator { + image: none; +} +)") + .arg(background, surface_hover, surface_pressed, border, text, disabled_text); +} + +QString location_edit_style_sheet(QPalette const& palette) +{ + auto surface = style_sheet_color(chrome_surface(palette)); + auto hover = style_sheet_color(chrome_surface_hover(palette)); + auto border = style_sheet_color(chrome_border(palette)); + auto accent = style_sheet_color(chrome_accent(palette)); + auto text = style_sheet_color(palette.color(QPalette::Text)); + auto placeholder = style_sheet_color(chrome_muted_text(palette)); + auto selection = style_sheet_color(chrome_accent(palette)); + auto selection_text = style_sheet_color(palette.color(QPalette::HighlightedText)); + + return QStringLiteral(R"( +QLineEdit#LadybirdLocationEdit { + color: %5; + background: %1; + border: 1px solid %3; + border-radius: 8px; + min-height: 30px; + padding: 3px 12px; + selection-background-color: %7; + selection-color: %8; +} + +QLineEdit#LadybirdLocationEdit:hover { + background: %2; +} + +QLineEdit#LadybirdLocationEdit:focus { + background: %1; + border-color: %4; +} + +QLineEdit#LadybirdLocationEdit:disabled { + color: %6; +} +)") + .arg(surface, hover, border, accent, text, placeholder, selection, selection_text); +} + +QString bookmarks_bar_style_sheet(QPalette const& palette) +{ + auto background = style_sheet_color(chrome_background(palette)); + auto hover = style_sheet_color(chrome_surface_hover(palette)); + auto pressed = style_sheet_color(chrome_surface_pressed(palette)); + auto border = style_sheet_color(chrome_border(palette)); + auto text = style_sheet_color(palette.color(QPalette::ButtonText)); + + return QStringLiteral(R"( +QToolBar#LadybirdBookmarksBar { + color: %5; + background: %1; + border: 0; + border-bottom: 1px solid %4; + padding: 4px 8px; + spacing: 3px; +} + +QToolBar#LadybirdBookmarksBar QToolButton { + color: %5; + background: transparent; + border: 1px solid transparent; + border-radius: 7px; + padding: 4px 7px; +} + +QToolBar#LadybirdBookmarksBar QToolButton:hover { + background: %2; + border-color: %4; +} + +QToolBar#LadybirdBookmarksBar QToolButton:pressed, +QToolBar#LadybirdBookmarksBar QToolButton:checked { + background: %3; + border-color: %4; +} +)") + .arg(background, hover, pressed, border, text); +} + +QString find_in_page_style_sheet(QPalette const& palette) +{ + auto background = style_sheet_color(chrome_background(palette)); + auto surface = style_sheet_color(chrome_surface(palette)); + auto hover = style_sheet_color(chrome_surface_hover(palette)); + auto pressed = style_sheet_color(chrome_surface_pressed(palette)); + auto border = style_sheet_color(chrome_border(palette)); + auto accent = style_sheet_color(chrome_accent(palette)); + auto text = style_sheet_color(palette.color(QPalette::Text)); + auto muted = style_sheet_color(chrome_muted_text(palette)); + + return QStringLiteral(R"( +QWidget#LadybirdFindInPageBar { + background: %1; + border-top: 1px solid %5; +} + +QWidget#LadybirdFindInPageBar QLineEdit { + color: %7; + background: %2; + border: 1px solid %5; + border-radius: 8px; + min-height: 26px; + padding: 2px 9px; + selection-background-color: %6; +} + +QWidget#LadybirdFindInPageBar QLineEdit:focus { + border-color: %6; +} + +QWidget#LadybirdFindInPageBar QPushButton { + color: %7; + background: transparent; + border: 1px solid transparent; + border-radius: 7px; + min-height: 26px; +} + +QWidget#LadybirdFindInPageBar QPushButton:hover { + background: %3; + border-color: %5; +} + +QWidget#LadybirdFindInPageBar QPushButton:pressed { + background: %4; + border-color: %5; +} + +QWidget#LadybirdFindInPageBar QCheckBox, +QWidget#LadybirdFindInPageBar QLabel { + color: %8; +} +)") + .arg(background, surface, hover, pressed, border, accent, text, muted); +} + +QString tab_widget_style_sheet(QPalette const& palette) +{ + auto background = style_sheet_color(chrome_background(palette)); + auto hover = style_sheet_color(chrome_surface_hover(palette)); + auto pressed = style_sheet_color(chrome_surface_pressed(palette)); + auto border = style_sheet_color(chrome_border(palette)); + auto text = style_sheet_color(palette.color(QPalette::ButtonText)); + auto close_hover = style_sheet_color(QColor(196, 43, 28)); + auto close_text = style_sheet_color(QColor(255, 255, 255)); + + return QStringLiteral(R"( +QWidget#LadybirdTabStrip { + color: %5; + background: %1; + border: 0; + border-bottom: 1px solid %4; +} + +QToolButton#LadybirdNewTabButton, +QPushButton#LadybirdTabButton { + color: %5; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + min-width: 24px; + min-height: 24px; + padding: 0; +} + +QToolButton#LadybirdNewTabButton:hover, +QPushButton#LadybirdTabButton:hover { + background: %2; + border-color: %4; +} + +QToolButton#LadybirdNewTabButton:pressed, +QPushButton#LadybirdTabButton:pressed, +QPushButton#LadybirdTabButton:checked { + background: %3; + border-color: %4; +} + +QToolButton#LadybirdWindowButton, +QToolButton#LadybirdCloseWindowButton { + color: %5; + background: transparent; + border: 0; + border-radius: 0; + min-width: 40px; + min-height: 32px; + padding: 0; +} + +QToolButton#LadybirdWindowButton:hover { + background: %2; +} + +QToolButton#LadybirdWindowButton:pressed { + background: %3; +} + +QToolButton#LadybirdCloseWindowButton:hover { + color: %7; + background: %6; +} + +QToolButton#LadybirdCloseWindowButton:pressed { + color: %7; + background: %6; +} +)") + .arg(background, hover, pressed, border, text, close_hover, close_text); +} + +QString autocomplete_popup_style_sheet(QPalette const& palette) +{ + auto surface = style_sheet_color(chrome_surface(palette)); + auto border = style_sheet_color(chrome_border(palette)); + auto text = style_sheet_color(palette.color(QPalette::Text)); + + return QStringLiteral(R"( +QFrame#LadybirdAutocompletePopup { + color: %3; + background: %1; + border: 1px solid %2; + border-radius: 10px; +} + +QListView#LadybirdAutocompleteList { + color: %3; + background: transparent; + border: 0; + outline: 0; +} +)") + .arg(surface, border, text); +} + +} diff --git a/UI/Qt/ChromeStyle.h b/UI/Qt/ChromeStyle.h new file mode 100644 index 0000000000..da0ec7f339 --- /dev/null +++ b/UI/Qt/ChromeStyle.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Ladybird::ChromeStyle { + +QColor chrome_background(QPalette const&); +QColor chrome_surface(QPalette const&); +QColor chrome_surface_hover(QPalette const&); +QColor chrome_surface_pressed(QPalette const&); +QColor chrome_border(QPalette const&); +QColor chrome_accent(QPalette const&); +QColor chrome_muted_text(QPalette const&); + +QString style_sheet_color(QColor const&); +QString navigation_toolbar_style_sheet(QPalette const&); +QString location_edit_style_sheet(QPalette const&); +QString bookmarks_bar_style_sheet(QPalette const&); +QString find_in_page_style_sheet(QPalette const&); +QString tab_widget_style_sheet(QPalette const&); +QString autocomplete_popup_style_sheet(QPalette const&); + +} diff --git a/UI/Qt/FindInPageWidget.cpp b/UI/Qt/FindInPageWidget.cpp index 4fe0f58700..2ebe9e95f5 100644 --- a/UI/Qt/FindInPageWidget.cpp +++ b/UI/Qt/FindInPageWidget.cpp @@ -4,11 +4,13 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include #include +#include #include namespace Ladybird { @@ -18,8 +20,10 @@ FindInPageWidget::FindInPageWidget(Tab* tab, WebContentView* content_view) , m_tab(tab) , m_content_view(content_view) { + setObjectName("LadybirdFindInPageBar"); setFocusPolicy(Qt::FocusPolicy::StrongFocus); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + update_chrome_style(); auto* layout = new QHBoxLayout(this); setLayout(layout); @@ -88,6 +92,28 @@ FindInPageWidget::FindInPageWidget(Tab* tab, WebContentView* content_view) FindInPageWidget::~FindInPageWidget() = default; +bool FindInPageWidget::event(QEvent* event) +{ + if (event->type() == QEvent::PaletteChange) { + update_chrome_style(); + m_previous_button->setIcon(create_tvg_icon_with_theme_colors("up", palette())); + m_next_button->setIcon(create_tvg_icon_with_theme_colors("down", palette())); + m_exit_button->setIcon(create_tvg_icon_with_theme_colors("close", palette())); + } + + return QWidget::event(event); +} + +void FindInPageWidget::update_chrome_style() +{ + if (m_is_updating_chrome_style) + return; + + m_is_updating_chrome_style = true; + setStyleSheet(ChromeStyle::find_in_page_style_sheet(palette())); + m_is_updating_chrome_style = false; +} + void FindInPageWidget::find_text_changed() { auto query = ak_string_from_qstring(m_find_text->text()); diff --git a/UI/Qt/FindInPageWidget.h b/UI/Qt/FindInPageWidget.h index cfb60e30b9..e7dc964615 100644 --- a/UI/Qt/FindInPageWidget.h +++ b/UI/Qt/FindInPageWidget.h @@ -36,12 +36,14 @@ public: public slots: private: + virtual bool event(QEvent*) override; virtual void keyPressEvent(QKeyEvent*) override; virtual void focusInEvent(QFocusEvent*) override; virtual void showEvent(QShowEvent*) override; virtual void hideEvent(QHideEvent*) override; void find_text_changed(); + void update_chrome_style(); Tab* m_tab { nullptr }; WebContentView* m_content_view { nullptr }; @@ -52,6 +54,7 @@ private: QPushButton* m_exit_button { nullptr }; QCheckBox* m_match_case { nullptr }; QLabel* m_result_label { nullptr }; + bool m_is_updating_chrome_style { false }; }; } diff --git a/UI/Qt/LocationEdit.cpp b/UI/Qt/LocationEdit.cpp index 0710b8ed5a..c83b36b2c9 100644 --- a/UI/Qt/LocationEdit.cpp +++ b/UI/Qt/LocationEdit.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,10 @@ LocationEdit::LocationEdit(QWidget* parent) : QLineEdit(parent) , m_autocomplete(new Autocomplete(this)) { + setObjectName("LadybirdLocationEdit"); + setMinimumHeight(34); + update_chrome_style(); + m_leading_icon_action = addAction(create_tvg_icon_with_theme_colors("search", palette()), QLineEdit::LeadingPosition); m_leading_icon_action->setToolTip("Search"); @@ -229,8 +234,10 @@ LocationEdit::LocationEdit(QWidget* parent) void LocationEdit::changeEvent(QEvent* event) { QLineEdit::changeEvent(event); - if (event->type() == QEvent::PaletteChange) + if (event->type() == QEvent::PaletteChange) { + update_chrome_style(); update_location_icon(); + } } void LocationEdit::focusInEvent(QFocusEvent* event) @@ -304,6 +311,16 @@ void LocationEdit::search_engine_changed() update_placeholder(); } +void LocationEdit::update_chrome_style() +{ + if (m_is_updating_chrome_style) + return; + + m_is_updating_chrome_style = true; + setStyleSheet(ChromeStyle::location_edit_style_sheet(palette())); + m_is_updating_chrome_style = false; +} + void LocationEdit::update_placeholder() { if (auto const& search_engine = WebView::Application::settings().search_engine(); search_engine.has_value()) { diff --git a/UI/Qt/LocationEdit.h b/UI/Qt/LocationEdit.h index d4db1f4d42..3cf8fdcf5d 100644 --- a/UI/Qt/LocationEdit.h +++ b/UI/Qt/LocationEdit.h @@ -49,6 +49,7 @@ private: virtual void search_engine_changed() override; void update_placeholder(); + void update_chrome_style(); void update_location_icon(); void update_loading_icon(); void highlight_location(); @@ -69,6 +70,7 @@ private: QIcon m_favicon; bool m_url_is_hidden { false }; bool m_is_loading { false }; + bool m_is_updating_chrome_style { false }; int m_loading_animation_frame { 0 }; bool m_is_applying_inline_autocomplete { false }; diff --git a/UI/Qt/Tab.cpp b/UI/Qt/Tab.cpp index a4b625b2a6..93bbc9db8c 100644 --- a/UI/Qt/Tab.cpp +++ b/UI/Qt/Tab.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,7 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, m_find_in_page = new FindInPageWidget(this, m_view); m_find_in_page->setVisible(false); m_toolbar = new QToolBar(this); + m_toolbar->setObjectName("LadybirdNavigationToolbar"); m_location_edit = new LocationEdit(this); m_bookmarks_bar = new BookmarksBar(this); m_loading_animation_timer = new QTimer(this); @@ -121,7 +123,6 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, m_hamburger_button->setIcon(create_tvg_icon_with_theme_colors("hamburger", palette())); m_hamburger_button->setPopupMode(QToolButton::InstantPopup); m_hamburger_button->setMenu(&m_window->hamburger_menu()); - m_hamburger_button->setStyleSheet(":menu-indicator {image: none}"); QObject::connect(&m_window->hamburger_menu(), &QMenu::aboutToShow, m_hamburger_button, [this]() { m_hamburger_button->setDown(true); @@ -153,9 +154,9 @@ Tab::Tab(BrowserWindow* window, RefPtr parent_client, m_toolbar->setIconSize({ 16, 16 }); m_toolbar->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - // This is a little awkward, but without this Qt shrinks the button to the size of the icon. - // Note: toolButtonStyle="0" -> ToolButtonIconOnly. - m_toolbar->setStyleSheet("QToolButton[toolButtonStyle=\"0\"]{padding:0px 4px 0px 4px;height:24px}"); + m_toolbar->setMovable(false); + m_toolbar->setFloatable(false); + update_chrome_style(); m_hamburger_button_action->setVisible(!Settings::the()->show_menubar()); @@ -609,6 +610,7 @@ void Tab::update_hover_label() bool Tab::event(QEvent* event) { if (event->type() == QEvent::PaletteChange) { + update_chrome_style(); recreate_toolbar_icons(); if (m_is_loading) update_tab_icon(); @@ -618,6 +620,17 @@ bool Tab::event(QEvent* event) return QWidget::event(event); } +void Tab::update_chrome_style() +{ + if (m_is_updating_chrome_style) + return; + + m_is_updating_chrome_style = true; + m_toolbar->setStyleSheet(ChromeStyle::navigation_toolbar_style_sheet(palette())); + m_hover_label->setStyleSheet(QStringLiteral("padding: 2px 6px; border-radius: 6px;")); + m_is_updating_chrome_style = false; +} + void Tab::recreate_toolbar_icons() { m_navigate_back_action->setIcon(create_tvg_icon_with_theme_colors("back", palette())); diff --git a/UI/Qt/Tab.h b/UI/Qt/Tab.h index 8dc8132b2d..6dce05e3ce 100644 --- a/UI/Qt/Tab.h +++ b/UI/Qt/Tab.h @@ -100,6 +100,7 @@ private: virtual void config_variable_changed(WebView::ConfigVariableID) override; void recreate_toolbar_icons(); + void update_chrome_style(); void update_tab_title(); void set_loading(bool); void update_tab_icon(); @@ -119,6 +120,7 @@ private: QIcon m_favicon; QTimer* m_loading_animation_timer { nullptr }; bool m_is_loading { false }; + bool m_is_updating_chrome_style { false }; int m_loading_animation_frame { 0 }; QMenu* m_context_menu { nullptr }; diff --git a/UI/Qt/TabBar.cpp b/UI/Qt/TabBar.cpp index 2b9929e00d..fa9698d2de 100644 --- a/UI/Qt/TabBar.cpp +++ b/UI/Qt/TabBar.cpp @@ -7,55 +7,45 @@ */ #include +#include #include #include #include #include #include +#include #include #include +#include #include -#include -#include +#include +#include #include #include +#include namespace Ladybird { -class LeftAlignedTabStyle final : public QProxyStyle { -public: - virtual void drawControl(ControlElement element, QStyleOption const* option, QPainter* painter, QWidget const* widget = nullptr) const override - { - if (element != QStyle::CE_TabBarTabLabel) { - QProxyStyle::drawControl(element, option, painter, widget); - return; - } - - auto const* tab = qstyleoption_cast(option); - if (!tab) { - QProxyStyle::drawControl(element, option, painter, widget); - return; - } - - QStyleOptionTab tab_without_text(*tab); - tab_without_text.text.clear(); - QProxyStyle::drawControl(element, &tab_without_text, painter, widget); - - auto text_rect = subElementRect(QStyle::SE_TabBarTabText, tab, widget); - int text_alignment = Qt::AlignLeft | Qt::AlignVCenter | Qt::TextShowMnemonic; - if (!styleHint(QStyle::SH_UnderlineShortcut, tab, widget)) - text_alignment |= Qt::TextHideMnemonic; - - drawItemText(painter, text_rect, text_alignment, tab->palette, tab->state & QStyle::State_Enabled, tab->text, - widget ? widget->foregroundRole() : QPalette::WindowText); - } -}; +static QPainterPath active_tab_path(QRectF const& rect, qreal radius) +{ + QPainterPath path; + path.moveTo(rect.left(), rect.bottom()); + path.lineTo(rect.left(), rect.top() + radius); + path.quadTo(rect.left(), rect.top(), rect.left() + radius, rect.top()); + path.lineTo(rect.right() - radius, rect.top()); + path.quadTo(rect.right(), rect.top(), rect.right(), rect.top() + radius); + path.lineTo(rect.right(), rect.bottom()); + path.closeSubpath(); + return path; +} TabBar::TabBar(TabWidget* tab_widget) : QTabBar(tab_widget) , m_tab_widget(tab_widget) { + setMouseTracking(true); + setMinimumHeight(38); } void TabBar::set_available_width(int width) @@ -72,21 +62,106 @@ QSize TabBar::tabSizeHint(int index) const if (auto count = this->count(); count > 0) { auto width = (m_available_width > 0 ? m_available_width : this->width()) / count; - width = min(225, width); + width = min(240, width); width = max(128, width); hint.setWidth(width); } + hint.setHeight(34); return hint; } +void TabBar::paintEvent(QPaintEvent* event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.fillRect(rect(), ChromeStyle::chrome_background(palette())); + + auto border = ChromeStyle::chrome_border(palette()); + auto accent = ChromeStyle::chrome_accent(palette()); + auto text_color = palette().color(QPalette::Text); + auto muted_text_color = ChromeStyle::chrome_muted_text(palette()); + + for (int index = 0; index < count(); ++index) { + auto tab_rect = tabRect(index); + if (!tab_rect.isValid() || tab_rect.right() < 0 || tab_rect.left() > width()) + continue; + + bool is_selected = index == currentIndex(); + bool is_hovered = index == m_hovered_tab_index; + + if (is_selected || is_hovered) { + auto shape_rect = QRectF(tab_rect).adjusted(2.0, is_selected ? 4.0 : 7.0, -2.0, is_selected ? 0.0 : -6.0); + auto surface = is_selected ? ChromeStyle::chrome_surface(palette()) : ChromeStyle::chrome_surface_hover(palette()); + + auto tab_path = is_selected + ? active_tab_path(shape_rect, 7.0) + : [&] { + QPainterPath path; + path.addRoundedRect(shape_rect, 7.0, 7.0); + return path; + }(); + + painter.setBrush(surface); + painter.setPen(is_selected ? QPen(border, 1) : Qt::NoPen); + painter.drawPath(tab_path); + + if (is_selected) { + painter.setPen(QPen(accent, 2)); + painter.drawLine(QPointF(shape_rect.left() + 9.0, shape_rect.top() + 1.0), + QPointF(shape_rect.right() - 9.0, shape_rect.top() + 1.0)); + } + } else if (index > 0 && index != currentIndex() + 1) { + auto separator = border; + separator.setAlpha(110); + painter.setPen(separator); + painter.drawLine(tab_rect.topLeft() + QPoint(0, 11), tab_rect.bottomLeft() - QPoint(0, 10)); + } + + auto contents_rect = tab_rect.adjusted(13, 1, -13, 0); + if (auto* left_button = tabButton(index, QTabBar::LeftSide); left_button && left_button->isVisible()) + contents_rect.setLeft(max(contents_rect.left(), left_button->geometry().right() + 6)); + if (auto* right_button = tabButton(index, QTabBar::RightSide); right_button && right_button->isVisible()) + contents_rect.setRight(min(contents_rect.right(), right_button->geometry().left() - 6)); + + auto icon = tabIcon(index); + if (!icon.isNull() && contents_rect.width() > 26) { + constexpr int icon_size = 16; + QRect icon_rect { + contents_rect.left(), + contents_rect.top() + (contents_rect.height() - icon_size) / 2, + icon_size, + icon_size, + }; + icon.paint(&painter, icon_rect); + contents_rect.setLeft(icon_rect.right() + 7); + } + + QFont tab_font = font(); + if (is_selected) + tab_font.setWeight(QFont::DemiBold); + painter.setFont(tab_font); + + QFontMetrics font_metrics(tab_font); + auto title = font_metrics.elidedText(tabText(index), Qt::ElideRight, max(0, contents_rect.width())); + painter.setPen(is_selected || is_hovered ? text_color : muted_text_color); + painter.drawText(contents_rect, Qt::AlignLeft | Qt::AlignVCenter, title); + } +} + void TabBar::contextMenuEvent(QContextMenuEvent* event) { if (!m_tab_widget) return; - if (auto* tab = m_tab_widget->tab(tabAt(event->pos()))) + auto tab_index = tabAt(event->pos()); + if (tab_index < 0) + return; + + if (auto* tab = m_tab_widget->tab(tab_index)) tab->context_menu()->exec(event->globalPos()); } @@ -94,8 +169,16 @@ void TabBar::mousePressEvent(QMouseEvent* event) { event->ignore(); - auto rect_of_current_tab = tabRect(tabAt(event->pos())); - m_x_position_in_selected_tab_while_dragging = event->pos().x() - rect_of_current_tab.x(); + auto pressed_tab = tabAt(event->pos()); + if (pressed_tab < 0 && event->button() == Qt::LeftButton) { + if (start_window_move()) + return; + } + + if (pressed_tab >= 0) { + auto rect_of_current_tab = tabRect(pressed_tab); + m_x_position_in_selected_tab_while_dragging = event->pos().x() - rect_of_current_tab.x(); + } QTabBar::mousePressEvent(event); } @@ -104,6 +187,16 @@ void TabBar::mouseMoveEvent(QMouseEvent* event) { event->ignore(); + if (auto hovered_tab = tabAt(event->pos()); hovered_tab != m_hovered_tab_index) { + m_hovered_tab_index = hovered_tab; + update(); + } + + if (count() == 0) { + QTabBar::mouseMoveEvent(event); + return; + } + auto rect_of_first_tab = tabRect(0); auto rect_of_last_tab = tabRect(count() - 1); @@ -123,13 +216,45 @@ void TabBar::mouseMoveEvent(QMouseEvent* event) } } +void TabBar::leaveEvent(QEvent* event) +{ + m_hovered_tab_index = -1; + update(); + QTabBar::leaveEvent(event); +} + +void TabBar::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (tabAt(event->pos()) < 0 && event->button() == Qt::LeftButton) { + toggle_window_maximized(); + event->accept(); + return; + } + + QTabBar::mouseDoubleClickEvent(event); +} + +bool TabBar::start_window_move() +{ + auto* handle = window()->windowHandle(); + if (!handle) + return false; + return handle->startSystemMove(); +} + +void TabBar::toggle_window_maximized() +{ + auto* top_level_window = window(); + if (top_level_window->isMaximized()) + top_level_window->showNormal(); + else + top_level_window->showMaximized(); +} + TabWidget::TabWidget(QWidget* parent) : QWidget(parent) { m_tab_bar = new TabBar(this); - auto* tab_style = new LeftAlignedTabStyle; - tab_style->setParent(m_tab_bar); - m_tab_bar->setStyle(tab_style); m_tab_bar->setDocumentMode(true); m_tab_bar->setElideMode(Qt::TextElideMode::ElideRight); @@ -142,27 +267,48 @@ TabWidget::TabWidget(QWidget* parent) m_stacked_widget = new QStackedWidget(this); m_new_tab_button = new QToolButton(this); + m_new_tab_button->setObjectName("LadybirdNewTabButton"); m_new_tab_button->setIconSize(QSize(16, 16)); - m_new_tab_button->setAutoRaise(true); m_new_tab_button->setToolTip("New Tab"); + m_minimize_window_button = new QToolButton(this); + m_minimize_window_button->setObjectName("LadybirdWindowButton"); + m_minimize_window_button->setToolTip("Minimize"); + m_minimize_window_button->setIconSize(QSize(14, 14)); + + m_maximize_window_button = new QToolButton(this); + m_maximize_window_button->setObjectName("LadybirdWindowButton"); + m_maximize_window_button->setIconSize(QSize(14, 14)); + + m_close_window_button = new QToolButton(this); + m_close_window_button->setObjectName("LadybirdCloseWindowButton"); + m_close_window_button->setToolTip("Close"); + m_close_window_button->setIconSize(QSize(14, 14)); + recreate_icons(); auto* tab_bar_row_layout = new QHBoxLayout(); tab_bar_row_layout->setSpacing(0); - tab_bar_row_layout->setContentsMargins(0, 0, 0, 0); + tab_bar_row_layout->setContentsMargins(8, 0, 8, 0); tab_bar_row_layout->addWidget(m_tab_bar); tab_bar_row_layout->addWidget(m_new_tab_button); tab_bar_row_layout->addStretch(1); + tab_bar_row_layout->addWidget(m_minimize_window_button); + tab_bar_row_layout->addWidget(m_maximize_window_button); + tab_bar_row_layout->addWidget(m_close_window_button); m_tab_bar_row = new QWidget(this); + m_tab_bar_row->setObjectName("LadybirdTabStrip"); + m_tab_bar_row->setMinimumHeight(40); m_tab_bar_row->setLayout(tab_bar_row_layout); + m_tab_bar_row->installEventFilter(this); auto* main_layout = new QVBoxLayout(this); main_layout->setSpacing(0); main_layout->setContentsMargins(0, 0, 0, 0); main_layout->addWidget(m_tab_bar_row); main_layout->addWidget(m_stacked_widget, 1); + update_chrome_style(); connect(m_tab_bar, &QTabBar::currentChanged, this, [this](int index) { if (index >= 0 && index < m_stacked_widget->count()) @@ -182,6 +328,16 @@ TabWidget::TabWidget(QWidget* parent) m_stacked_widget->insertWidget(to, widget); m_stacked_widget->setCurrentIndex(m_tab_bar->currentIndex()); }); + + connect(m_minimize_window_button, &QToolButton::clicked, this, [this] { + window()->showMinimized(); + }); + connect(m_maximize_window_button, &QToolButton::clicked, this, [this] { + toggle_window_maximized(); + }); + connect(m_close_window_button, &QToolButton::clicked, this, [this] { + window()->close(); + }); } void TabWidget::add_tab(Tab* widget, QString const& label) @@ -239,11 +395,35 @@ bool TabWidget::event(QEvent* event) } } else if (type == QEvent::PaletteChange) { recreate_icons(); + update_chrome_style(); + } else if (type == QEvent::WindowStateChange) { + update_window_button_icons(); } return QWidget::event(event); } +bool TabWidget::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == m_tab_bar_row) { + if (event->type() == QEvent::MouseButtonDblClick) { + auto* mouse_event = static_cast(event); + if (mouse_event->button() == Qt::LeftButton) { + toggle_window_maximized(); + return true; + } + } + + if (event->type() == QEvent::MouseButtonPress) { + auto* mouse_event = static_cast(event); + if (mouse_event->button() == Qt::LeftButton && start_window_move()) + return true; + } + } + + return QWidget::eventFilter(watched, event); +} + void TabWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); @@ -261,12 +441,53 @@ void TabWidget::update_tab_layout() void TabWidget::recreate_icons() { m_new_tab_button->setIcon(create_tvg_icon_with_theme_colors("new_tab", palette())); + update_window_button_icons(); +} + +void TabWidget::update_chrome_style() +{ + if (m_is_updating_chrome_style) + return; + + m_is_updating_chrome_style = true; + m_tab_bar_row->setStyleSheet(ChromeStyle::tab_widget_style_sheet(palette())); + m_is_updating_chrome_style = false; +} + +void TabWidget::update_window_button_icons() +{ + auto* top_level_window = window(); + auto* button_style = top_level_window->style(); + m_minimize_window_button->setIcon(button_style->standardIcon(QStyle::SP_TitleBarMinButton)); + m_maximize_window_button->setIcon(button_style->standardIcon(top_level_window->isMaximized() ? QStyle::SP_TitleBarNormalButton : QStyle::SP_TitleBarMaxButton)); + m_maximize_window_button->setToolTip(top_level_window->isMaximized() ? "Restore" : "Maximize"); + m_close_window_button->setIcon(button_style->standardIcon(QStyle::SP_TitleBarCloseButton)); +} + +void TabWidget::toggle_window_maximized() +{ + auto* top_level_window = window(); + if (top_level_window->isMaximized()) + top_level_window->showNormal(); + else + top_level_window->showMaximized(); + update_window_button_icons(); +} + +bool TabWidget::start_window_move() +{ + auto* handle = window()->windowHandle(); + if (!handle) + return false; + return handle->startSystemMove(); } TabBarButton::TabBarButton(QIcon const& icon, QWidget* parent) : QPushButton(icon, {}, parent) { - resize({ 20, 20 }); + setObjectName("LadybirdTabButton"); + setFixedSize({ 24, 24 }); + setIconSize({ 14, 14 }); setFlat(true); } diff --git a/UI/Qt/TabBar.h b/UI/Qt/TabBar.h index e683e560ff..d685bf5132 100644 --- a/UI/Qt/TabBar.h +++ b/UI/Qt/TabBar.h @@ -20,6 +20,8 @@ class QAction; class QContextMenuEvent; class QEvent; class QIcon; +class QMouseEvent; +class QPaintEvent; class QToolButton; namespace Ladybird { @@ -39,12 +41,19 @@ public: virtual void contextMenuEvent(QContextMenuEvent* event) override; private: + virtual void paintEvent(QPaintEvent*) override; + virtual void leaveEvent(QEvent*) override; + virtual void mouseDoubleClickEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; void mouseMoveEvent(QMouseEvent*) override; + bool start_window_move(); + void toggle_window_maximized(); + QPointer m_tab_widget; int m_available_width { 0 }; + int m_hovered_tab_index { -1 }; int m_x_position_in_selected_tab_while_dragging { 0 }; }; @@ -82,16 +91,25 @@ signals: protected: virtual bool event(QEvent* event) override; + virtual bool eventFilter(QObject*, QEvent*) override; virtual void resizeEvent(QResizeEvent*) override; private: void update_tab_layout(); void recreate_icons(); + void update_chrome_style(); + void update_window_button_icons(); + void toggle_window_maximized(); + bool start_window_move(); TabBar* m_tab_bar { nullptr }; QStackedWidget* m_stacked_widget { nullptr }; QToolButton* m_new_tab_button { nullptr }; + QToolButton* m_minimize_window_button { nullptr }; + QToolButton* m_maximize_window_button { nullptr }; + QToolButton* m_close_window_button { nullptr }; QWidget* m_tab_bar_row { nullptr }; + bool m_is_updating_chrome_style { false }; }; class TabBarButton final : public QPushButton {