ladybird/UI/Qt/TabBar.h
Andreas Kling 7b6e94a445 UI/Qt: Add tab hover previews
Show a delayed preview popup when hovering background tabs. Build the
thumbnail from the cached front or backup WebContent bitmap so chrome
does not need a fresh screenshot request or WebContent repaint on hover.

Tie the popup to the tab hover lifecycle and hide it on current-tab
hovers, tab presses, drags, layout changes, and tab strip exits.
2026-06-11 13:09:32 +02:00

261 lines
8.9 KiB
C++

/*
* Copyright (c) 2024-2026, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/TypeCasts.h>
#include <QPointer>
#include <QPushButton>
#include <QStackedWidget>
#include <QTabBar>
#include <QWidget>
class QAction;
class QBoxLayout;
class QContextMenuEvent;
class QDragEnterEvent;
class QDragLeaveEvent;
class QDragMoveEvent;
class QDropEvent;
class QEvent;
class QIcon;
class QMouseEvent;
class QPaintEvent;
class QPixmap;
class QResizeEvent;
class QToolButton;
class QTimer;
class QVariantAnimation;
class QWheelEvent;
namespace Ladybird {
class Tab;
class TabPreviewPopup;
class TabWidget;
enum class TabLayout : u8 {
Horizontal,
VerticalCollapsed,
VerticalExpanded,
};
class TabBar final : public QTabBar {
Q_OBJECT
public:
explicit TabBar(TabWidget*);
void set_available_width(int width);
TabLayout tab_layout() const { return m_tab_layout; }
void set_tab_layout(TabLayout);
void refresh_tab_layout();
void recreate_icons();
private:
virtual QSize sizeHint() const override;
virtual QSize minimumSizeHint() const override;
virtual QSize tabSizeHint(int index) const override;
virtual void resizeEvent(QResizeEvent*) override;
virtual void tabLayoutChange() override;
virtual bool event(QEvent*) override;
virtual void paintEvent(QPaintEvent*) override;
virtual void contextMenuEvent(QContextMenuEvent* event) override;
virtual void dragEnterEvent(QDragEnterEvent*) override;
virtual void dragLeaveEvent(QDragLeaveEvent*) override;
virtual void dragMoveEvent(QDragMoveEvent*) override;
virtual void dropEvent(QDropEvent*) override;
virtual bool eventFilter(QObject*, QEvent*) override;
virtual void leaveEvent(QEvent*) override;
virtual void mouseDoubleClickEvent(QMouseEvent*) override;
virtual void mousePressEvent(QMouseEvent*) override;
virtual void mouseMoveEvent(QMouseEvent*) override;
virtual void mouseReleaseEvent(QMouseEvent*) override;
virtual void wheelEvent(QWheelEvent*) override;
int insertion_index_at(QPoint const&) const;
int drop_indicator_index_for_insertion_index(int insertion_index) const;
QPixmap render_tab_drag_pixmap(int index) const;
void toggle_window_maximized();
bool start_window_move();
void start_tab_drag(int index);
void start_hover_animation(int tab_index, qreal target_progress);
void schedule_tab_preview(int index);
void show_tab_preview();
void hide_tab_preview();
QPoint tab_preview_position_for(int index, QSize const& popup_size) const;
QRect visual_tab_rect(int index) const;
int tab_index_at(QPoint const&) const;
QSize vertical_size_hint(int tab_count) const;
int max_vertical_scroll_offset() const;
void set_vertical_scroll_offset(int);
void set_hovered_tab_index(int);
void ensure_tab_visible(int index);
void update_tab_button_geometry();
QPointer<TabWidget> m_tab_widget;
TabLayout m_tab_layout { TabLayout::Horizontal };
int m_available_width { 0 };
int m_hovered_tab_index { -1 };
int m_hover_animation_tab_index { -1 };
int m_vertical_scroll_offset { 0 };
qreal m_hover_progress { 0.0 };
int m_drop_indicator_index { -1 };
QVariantAnimation* m_hover_animation { nullptr };
QIcon m_fallback_tab_icon;
QPointer<Tab> m_pressed_tab;
QPoint m_position_in_selected_tab_while_dragging;
QPoint m_drag_start_position;
QTimer* m_tab_preview_timer { nullptr };
TabPreviewPopup* m_tab_preview_popup { nullptr };
int m_tab_preview_index { -1 };
};
class TabWidget final : public QWidget {
Q_OBJECT
public:
explicit TabWidget(QWidget* parent = nullptr);
TabBar* tab_bar() const { return m_tab_bar; }
QWidget* tab_bar_row() const { return m_tab_bar_row; }
QRect tab_strip_global_rect() const;
void add_tab(Tab* widget, QString const& label);
void insert_tab(int index, Tab* widget, QString const& label);
void remove_tab(int index);
Tab* take_tab(int index);
int count() const { return m_tab_bar->count(); }
int current_index() const { return m_tab_bar->currentIndex(); }
void set_current_index(int index) { m_tab_bar->setCurrentIndex(index); }
Tab* tab(int index) const { return as<Tab>(m_stacked_widget->widget(index)); }
void set_current_tab(Tab*);
int index_of(Tab* widget) const;
void set_tab_text(int index, QString const& text) { m_tab_bar->setTabText(index, text); }
void set_tab_tooltip(int index, QString const& tip) { m_tab_bar->setTabToolTip(index, tip); }
void set_tab_icon(int index, QIcon const& icon) { m_tab_bar->setTabIcon(index, icon); }
void set_tab_bar_visible(bool visible);
void set_new_tab_action(QAction* action);
void set_window_controls_visible(bool);
void update_window_button_icons();
void set_vertical_tabs_enabled(bool);
void set_vertical_tabs_expanded(bool);
void set_vertical_tabs_expand_on_hover(bool);
void update_tab_button_visibility();
signals:
void current_tab_changed(int index);
void tab_close_requested(int index);
protected:
virtual void dragEnterEvent(QDragEnterEvent*) override;
virtual void dragMoveEvent(QDragMoveEvent*) override;
virtual void dropEvent(QDropEvent*) override;
virtual bool event(QEvent* event) override;
virtual bool eventFilter(QObject*, QEvent*) override;
virtual void resizeEvent(QResizeEvent*) override;
private:
TabLayout current_tab_layout() const;
QWidget* tab_drag_area_widget() const;
bool vertical_tabs_effectively_expanded() const;
bool can_expand_vertical_tabs_on_hover() const;
bool cursor_is_over_vertical_tabs() const;
int vertical_tabs_layout_width() 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 rebuild_page_column();
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();
QRect vertical_tabs_chrome_rect() const;
int vertical_tabs_tab_width() const;
void update_vertical_tabs_button_layout();
void update_tab_layout();
void update_tab_chrome_visibility();
void recreate_icons();
void update_chrome_style();
void update_vertical_tabs_overlay_geometry();
void set_vertical_tabs_hover_expanded(bool);
void defer_update_vertical_tabs_hover_expanded();
void update_vertical_tabs_hover_expanded();
void toggle_window_maximized();
bool start_window_move();
void accept_tab_drag(QDragMoveEvent*);
void accept_tab_drop(QDropEvent*, int index);
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_window_controls { nullptr };
QStackedWidget* m_toolbar_container { nullptr };
QWidget* m_page_column { nullptr };
QWidget* m_tab_bar_row { 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 };
QBoxLayout* m_main_layout { nullptr };
QBoxLayout* m_tab_bar_row_layout { nullptr };
QBoxLayout* m_page_column_layout { nullptr };
QBoxLayout* m_vertical_tab_bar_column_layout { nullptr };
QBoxLayout* m_vertical_tabs_content_layout { nullptr };
bool m_tab_bar_visible { true };
bool m_window_controls_visible { true };
bool m_vertical_tabs_enabled { false };
bool m_vertical_tabs_expanded { true };
bool m_vertical_tabs_expand_on_hover { false };
bool m_vertical_tabs_hover_expanded { false };
bool m_is_resizing_vertical_tabs { false };
bool m_is_updating_chrome_style { false };
int m_vertical_tabs_expanded_width { 0 };
int m_vertical_tabs_resize_start_global_x { 0 };
int m_vertical_tabs_resize_start_width { 0 };
};
class TabBarButton final : public QPushButton {
Q_OBJECT
public:
explicit TabBarButton(QIcon const& icon, QWidget* parent = nullptr);
void set_collapsed_vertical_overlay(bool);
protected:
virtual bool event(QEvent* event) override;
};
}