| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/frame/browser_view_layout.h" |
| |
| #include <algorithm> |
| |
| #include "base/feature_list.h" |
| #include "base/i18n/rtl.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/numerics/safe_math.h" |
| #include "base/observer_list.h" |
| #include "base/scoped_observation.h" |
| #include "base/trace_event/common/trace_event_common.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_features.h" |
| #include "chrome/browser/ui/find_bar/find_bar.h" |
| #include "chrome/browser/ui/find_bar/find_bar_controller.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/features.h" |
| #include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h" |
| #include "chrome/browser/ui/views/exclusive_access_bubble_views.h" |
| #include "chrome/browser/ui/views/frame/browser_frame_view.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/frame/browser_view_layout_delegate.h" |
| #include "chrome/browser/ui/views/frame/contents_layout_manager.h" |
| #include "chrome/browser/ui/views/frame/immersive_mode_controller.h" |
| #include "chrome/browser/ui/views/frame/multi_contents_view.h" |
| #include "chrome/browser/ui/views/frame/tab_strip_view_interface.h" |
| #include "chrome/browser/ui/views/frame/top_container_view.h" |
| #include "chrome/browser/ui/views/infobars/infobar_container_view.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "components/web_modal/web_contents_modal_dialog_host.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_observer.h" |
| #include "ui/views/window/client_view.h" |
| #include "ui/views/window/hit_test_utils.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "chrome/browser/ui/fullscreen_util_mac.h" |
| #endif |
| |
| using views::View; |
| using web_modal::ModalDialogHostObserver; |
| using web_modal::WebContentsModalDialogHost; |
| |
| namespace { |
| |
| // The number of pixels the constrained window should overlap the bottom |
| // of the omnibox. |
| const int kConstrainedWindowOverlap = 3; |
| |
| // The normal clipping created by `View::Paint()` may not cover the bottom of |
| // the TopContainerView at certain scale factor because both of the position and |
| // the height might be roudned down. This function sets the clip path that |
| // enlarges the height at 2 DPs to compensate this error (both origin and size) |
| // that the canvas can cover the entire TopContainerView. See |
| // crbug.com/390669712 for more details. TODO(crbug.com/41344902): Remove this |
| // hack once the pixel canvas is enabled on all aura platforms. Note that macOS |
| // supports integer scale only, so this isn't necessary on macOS. |
| void SetClipPathWithBottomAllowance(views::View* view) { |
| if (!features::IsPixelCanvasRecordingEnabled()) { |
| constexpr int kBottomPaintAllowance = 2; |
| const gfx::Rect local_bounds = view->GetLocalBounds(); |
| const int extended_height = local_bounds.height() + kBottomPaintAllowance; |
| view->SetClipPath( |
| SkPath::Rect(SkRect::MakeWH(local_bounds.width(), extended_height))); |
| } |
| } |
| |
| } // namespace |
| |
| constexpr int BrowserViewLayout::kMainBrowserContentsMinimumWidth; |
| |
| struct BrowserViewLayout::ContentsContainerLayoutResult { |
| gfx::Rect contents_container_bounds; |
| gfx::Rect side_panel_bounds; |
| bool side_panel_visible; |
| bool side_panel_right_aligned; |
| bool contents_container_after_side_panel; |
| gfx::Rect separator_bounds; |
| }; |
| |
| class BrowserViewLayout::BrowserModalDialogHostViews |
| : public WebContentsModalDialogHost, |
| public views::WidgetObserver { |
| public: |
| explicit BrowserModalDialogHostViews(BrowserViewLayout* browser_view_layout) |
| : browser_view_layout_(browser_view_layout) { |
| // browser_view might be nullptr in unit tests. |
| if (browser_view_layout->browser_view_) { |
| browser_widget_observation_.Observe( |
| browser_view_layout->browser_view_->GetWidget()); |
| } |
| } |
| |
| BrowserModalDialogHostViews(const BrowserModalDialogHostViews&) = delete; |
| BrowserModalDialogHostViews& operator=(const BrowserModalDialogHostViews&) = |
| delete; |
| |
| ~BrowserModalDialogHostViews() override { |
| observer_list_.Notify(&ModalDialogHostObserver::OnHostDestroying); |
| } |
| |
| void NotifyPositionRequiresUpdate() { |
| observer_list_.Notify(&ModalDialogHostObserver::OnPositionRequiresUpdate); |
| } |
| |
| gfx::Point GetDialogPosition(const gfx::Size& dialog_size) override { |
| // Horizontally places the dialog at the center of the content. |
| |
| views::View* view = browser_view_layout_->contents_container_; |
| // Recalculate bounds of `contents_container_`. It may be stale due to |
| // pending layouts (from switching tabs, for example). The `top` and |
| // `bottom` parameters should not be relevant to the result, since we only |
| // care about the resulting width here. |
| const auto* browser_view = view->parent(); |
| |
| gfx::Rect view_bounds(browser_view->GetLocalBounds()); |
| view_bounds.set_y(view->bounds().y()); |
| view_bounds.set_height(view->bounds().bottom()); |
| |
| BrowserViewLayout::ContentsContainerLayoutResult layout_result = |
| browser_view_layout_->CalculateContentsContainerLayout(view_bounds); |
| |
| int leading_x; |
| if (base::i18n::IsRTL()) { |
| // Dialog coordinates are not flipped for RTL, but the View's coordinates |
| // are. Calculate the left edge of `contents_container_bounds`. |
| if (layout_result.contents_container_after_side_panel) { |
| leading_x = 0; |
| } else { |
| leading_x = browser_view->GetLocalBounds().width() - |
| layout_result.contents_container_bounds.width(); |
| } |
| } else { |
| leading_x = layout_result.contents_container_bounds.x(); |
| } |
| const int middle_x = |
| leading_x + layout_result.contents_container_bounds.width() / 2; |
| return gfx::Point(middle_x - dialog_size.width() / 2, |
| browser_view_layout_->dialog_top_y_); |
| } |
| |
| bool ShouldActivateDialog() const override { |
| // The browser Widget may be inactive if showing a bubble so instead check |
| // against the last active browser window when determining whether to |
| // activate the dialog. |
| return chrome::FindLastActive() == |
| browser_view_layout_->browser_view_->browser(); |
| } |
| |
| bool ShouldConstrainDialogBoundsByHost() override { |
| return !base::FeatureList::IsEnabled(features::kTabModalUsesDesktopWidget); |
| } |
| |
| gfx::Size GetMaximumDialogSize() override { |
| // Modals use NativeWidget and cannot be rendered beyond the browser |
| // window boundaries. Restricting them to the browser window bottom |
| // boundary and let the dialog to figure out a good layout. |
| // WARNING: previous attempts to allow dialog to extend beyond the browser |
| // boundaries have caused regressions in a number of dialogs. See |
| // crbug.com/364463378, crbug.com/369739216, crbug.com/363205507. |
| // TODO(crbug.com/334413759, crbug.com/346974105): use desktop widgets |
| // universally. |
| views::View* view = browser_view_layout_->contents_container_; |
| gfx::Rect content_area = view->ConvertRectToWidget(view->GetLocalBounds()); |
| const int top = browser_view_layout_->dialog_top_y_; |
| return gfx::Size(content_area.width(), content_area.bottom() - top); |
| } |
| |
| views::Widget* GetHostWidget() const { |
| return views::Widget::GetWidgetForNativeView( |
| browser_view_layout_->delegate_->GetHostViewForAnchoring()); |
| } |
| |
| // views::WidgetObserver: |
| void OnWidgetDestroying(views::Widget* browser_widget) override { |
| browser_widget_observation_.Reset(); |
| } |
| void OnWidgetBoundsChanged(views::Widget* browser_widget, |
| const gfx::Rect& new_bounds) override { |
| // Update the modal dialogs' position when the browser window bounds change. |
| // This is used to adjust the modal dialog's position when the browser |
| // window is being dragged across screen boundaries. We avoid having the |
| // modal dialog partially visible as it may display security-sensitive |
| // information. |
| NotifyPositionRequiresUpdate(); |
| } |
| |
| private: |
| gfx::NativeView GetHostView() const override { |
| views::Widget* const host_widget = GetHostWidget(); |
| return host_widget ? host_widget->GetNativeView() : gfx::NativeView(); |
| } |
| |
| // Add/remove observer. |
| void AddObserver(ModalDialogHostObserver* observer) override { |
| observer_list_.AddObserver(observer); |
| } |
| void RemoveObserver(ModalDialogHostObserver* observer) override { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| const raw_ptr<BrowserViewLayout> browser_view_layout_; |
| base::ScopedObservation<views::Widget, views::WidgetObserver> |
| browser_widget_observation_{this}; |
| |
| base::ObserverList<ModalDialogHostObserver>::Unchecked observer_list_; |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // BrowserViewLayout, public: |
| |
| BrowserViewLayout::BrowserViewLayout( |
| std::unique_ptr<BrowserViewLayoutDelegate> delegate, |
| BrowserView* browser_view, |
| views::View* window_scrim, |
| views::View* top_container, |
| WebAppFrameToolbarView* web_app_frame_toolbar, |
| views::Label* web_app_window_title, |
| TabStripRegionView* tab_strip_region_view, |
| views::View* vertical_tab_strip_container, |
| views::View* toolbar, |
| InfoBarContainerView* infobar_container, |
| views::View* main_container, |
| views::View* contents_container, |
| MultiContentsView* multi_contents_view, |
| views::View* left_aligned_side_panel_separator, |
| views::View* contents_height_side_panel, |
| views::View* right_aligned_side_panel_separator, |
| views::View* side_panel_rounded_corner, |
| views::View* top_container_separator) |
| : delegate_(std::move(delegate)), |
| browser_view_(browser_view), |
| window_scrim_(window_scrim), |
| top_container_(top_container), |
| web_app_frame_toolbar_(web_app_frame_toolbar), |
| web_app_window_title_(web_app_window_title), |
| tab_strip_region_view_(tab_strip_region_view), |
| vertical_tab_strip_container_(vertical_tab_strip_container), |
| toolbar_(toolbar), |
| infobar_container_(infobar_container), |
| main_container_(main_container), |
| contents_container_(contents_container), |
| multi_contents_view_(multi_contents_view), |
| contents_height_side_panel_(contents_height_side_panel), |
| left_aligned_side_panel_separator_(left_aligned_side_panel_separator), |
| right_aligned_side_panel_separator_(right_aligned_side_panel_separator), |
| side_panel_rounded_corner_(side_panel_rounded_corner), |
| top_container_separator_(top_container_separator), |
| tab_strip_(tab_strip_region_view_->tab_strip()), |
| dialog_host_(std::make_unique<BrowserModalDialogHostViews>(this)) {} |
| |
| BrowserViewLayout::~BrowserViewLayout() = default; |
| |
| void BrowserViewLayout::SetUseBrowserContentMinimumSize( |
| bool use_browser_content_minimum_size) { |
| use_browser_content_minimum_size_ = use_browser_content_minimum_size; |
| InvalidateLayout(); |
| } |
| |
| WebContentsModalDialogHost* BrowserViewLayout::GetWebContentsModalDialogHost() { |
| return dialog_host_.get(); |
| } |
| |
| gfx::Size BrowserViewLayout::GetMinimumSize(const views::View* host) const { |
| // Prevent having a 0x0 sized-contents as this can allow the window to be |
| // resized down such that it's invisible and can no longer accept events. |
| // Use a very small 1x1 size to allow the user and the web contents to be able |
| // to resize the window as small as possible without introducing bugs. |
| // https://crbug.com/847179. |
| constexpr gfx::Size kContentsMinimumSize(1, 1); |
| if (delegate_->GetBorderlessModeEnabled()) { |
| // The minimum size of a window is unrestricted for a borderless mode app. |
| return kContentsMinimumSize; |
| } |
| |
| // The minimum height for the normal (tabbed) browser window's contents area. |
| constexpr int kMainBrowserContentsMinimumHeight = 1; |
| |
| const bool has_tabstrip = |
| delegate_->SupportsWindowFeature(Browser::FEATURE_TABSTRIP); |
| const bool has_toolbar = |
| delegate_->SupportsWindowFeature(Browser::FEATURE_TOOLBAR); |
| const bool has_location_bar = |
| delegate_->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR); |
| const bool has_bookmarks_bar = |
| bookmark_bar_ && bookmark_bar_->GetVisible() && |
| delegate_->SupportsWindowFeature(Browser::FEATURE_BOOKMARKBAR); |
| |
| // TODO(crbug.com/437917495): Verify all callers have the correct bounds in |
| // vertical and horizontal tabstrip modes. |
| gfx::Size tabstrip_size( |
| has_tabstrip ? browser_view_->tab_strip_view()->GetMinimumSize() |
| : gfx::Size()); |
| gfx::Size toolbar_size((has_toolbar || has_location_bar) |
| ? toolbar_->GetMinimumSize() |
| : gfx::Size()); |
| gfx::Size bookmark_bar_size; |
| if (has_bookmarks_bar) { |
| bookmark_bar_size = bookmark_bar_->GetMinimumSize(); |
| } |
| gfx::Size infobar_container_size(infobar_container_->GetMinimumSize()); |
| // TODO(pkotwicz): Adjust the minimum height for the find bar. |
| |
| gfx::Size contents_size(contents_container_->GetMinimumSize()); |
| contents_size.SetToMax(use_browser_content_minimum_size_ |
| ? gfx::Size(kMainBrowserContentsMinimumWidth, |
| kMainBrowserContentsMinimumHeight) |
| : kContentsMinimumSize); |
| |
| const int min_height = |
| delegate_->GetTopInsetInBrowserView() + tabstrip_size.height() + |
| toolbar_size.height() + bookmark_bar_size.height() + |
| infobar_container_size.height() + contents_size.height(); |
| |
| const int min_width = std::max( |
| {tabstrip_size.width(), toolbar_size.width(), bookmark_bar_size.width(), |
| infobar_container_size.width(), contents_size.width()}); |
| |
| return gfx::Size(min_width, min_height); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // BrowserViewLayout, views::LayoutManager implementation: |
| |
| void BrowserViewLayout::Layout(views::View* browser_view) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::Layout"); |
| gfx::Rect available_bounds = browser_view->GetLocalBounds(); |
| |
| // The window scrim covers the entire browser view. |
| if (window_scrim_) { |
| window_scrim_->SetBoundsRect(available_bounds); |
| } |
| |
| if (tabs::IsVerticalTabsFeatureEnabled() && ShouldDisplayVerticalTabs()) { |
| LayoutVerticalTabStrip(available_bounds); |
| } |
| |
| available_bounds.set_y(available_bounds.y() + |
| delegate_->GetTopInsetInBrowserView()); |
| LayoutTitleBarForWebApp(available_bounds); |
| if (delegate_->ShouldLayoutTabStrip()) { |
| LayoutTabStripRegion(available_bounds); |
| if (delegate_->ShouldDrawTabStrip()) { |
| tab_strip_->SetBackgroundOffset(tab_strip_region_view_->GetMirroredX() + |
| browser_view_->GetMirroredX()); |
| } |
| LayoutWebUITabStrip(available_bounds); |
| } |
| LayoutToolbar(available_bounds); |
| |
| LayoutBookmarkAndInfoBars(available_bounds, browser_view->y()); |
| |
| // Top container requires updated toolbar and bookmark bar to compute bounds. |
| UpdateTopContainerBounds(available_bounds); |
| |
| // Layout the contents container in the remaining space. |
| // Ensure `available_bounds` has the correct height. |
| available_bounds.set_height(available_bounds.height() - available_bounds.y()); |
| LayoutContentsContainerView(available_bounds); |
| |
| // This must be done _after_ we lay out the WebContents since this |
| // code calls back into us to find the bounding box the find bar |
| // must be laid out within, and that code depends on the |
| // TabContentsContainer's bounds being up to date. |
| // |
| // Because Find Bar can be repositioned to keep from hiding find results, we |
| // don't want to reset its position on every layout, however - only if the |
| // geometry of the contents pane actually changes in a way that could affect |
| // the positioning of the bar. |
| const gfx::Rect new_contents_bounds = |
| contents_container_->GetBoundsInScreen(); |
| if (delegate_->HasFindBarController() && |
| (new_contents_bounds.width() != latest_contents_bounds_.width() || |
| (new_contents_bounds.y() != latest_contents_bounds_.y() && |
| new_contents_bounds.height() != latest_contents_bounds_.height()))) { |
| delegate_->MoveWindowForFindBarIfNecessary(); |
| } |
| latest_contents_bounds_ = new_contents_bounds; |
| |
| // Adjust the fullscreen exit bubble bounds for |top_container_|'s new bounds. |
| // This makes the fullscreen exit bubble look like it animates with |
| // |top_container_| in immersive fullscreen. |
| ExclusiveAccessBubbleViews* exclusive_access_bubble = |
| delegate_->GetExclusiveAccessBubble(); |
| if (exclusive_access_bubble) { |
| exclusive_access_bubble->RepositionIfVisible(); |
| } |
| |
| // Adjust any hosted dialogs if the browser's dialog hosting bounds changed. |
| const gfx::Rect dialog_bounds(dialog_host_->GetDialogPosition(gfx::Size()), |
| dialog_host_->GetMaximumDialogSize()); |
| const gfx::Rect host_widget_bounds = |
| dialog_host_->GetHostWidget() |
| ? dialog_host_->GetHostWidget()->GetClientAreaBoundsInScreen() |
| : gfx::Rect(); |
| const gfx::Rect dialog_bounds_in_screen = |
| dialog_bounds + host_widget_bounds.OffsetFromOrigin(); |
| if (latest_dialog_bounds_in_screen_ != dialog_bounds_in_screen) { |
| latest_dialog_bounds_in_screen_ = dialog_bounds_in_screen; |
| dialog_host_->NotifyPositionRequiresUpdate(); |
| } |
| } |
| |
| gfx::Size BrowserViewLayout::GetPreferredSize( |
| const views::View* host, |
| const views::SizeBounds& available_size) const { |
| return gfx::Size(); |
| } |
| |
| // Return the preferred size which is the size required to give each |
| // children their respective preferred size. |
| gfx::Size BrowserViewLayout::GetPreferredSize(const views::View* host) const { |
| return GetPreferredSize(host, {}); |
| } |
| |
| std::vector<raw_ptr<views::View, VectorExperimental>> |
| BrowserViewLayout::GetChildViewsInPaintOrder(const views::View* host) const { |
| std::vector<raw_ptr<views::View, VectorExperimental>> result = |
| views::LayoutManager::GetChildViewsInPaintOrder(host); |
| // Make sure `top_container_` is after `contents_container_` in paint order |
| // when this is a window using WindowControlsOverlay, to make sure the window |
| // controls are in fact drawn on top of the web contents. |
| if (delegate_->IsWindowControlsOverlayEnabled()) { |
| auto top_container_iter = std::ranges::find(result, top_container_); |
| |
| // TODO(crbug.com/445446905): For now `main_container_` only holds |
| // `contents_container_` and side panel related views. Once we are further |
| // along in the ToolbarHeightSidePanel effort, this function should be |
| // revisited and updated accordingly. |
| auto contents_container_iter = std::ranges::find(result, main_container_); |
| CHECK(contents_container_iter != result.end()); |
| // When in Immersive Fullscreen `top_container_` might not be one of our |
| // children at all. While Window Controls Overlay shouldn't be enabled in |
| // fullscreen either, during the transition there is a moment where both |
| // could be true at the same time. |
| if (top_container_iter != result.end()) { |
| std::rotate(top_container_iter, top_container_iter + 1, |
| contents_container_iter + 1); |
| } |
| } |
| return result; |
| } |
| |
| int BrowserViewLayout::GetMinWebContentsWidthForTesting() const { |
| return GetMinWebContentsWidth(); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // BrowserViewLayout, private: |
| |
| void BrowserViewLayout::LayoutTitleBarForWebApp(gfx::Rect& available_bounds) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::LayoutTitleBarForWebApp"); |
| if (!web_app_frame_toolbar_) { |
| return; |
| } |
| |
| if (delegate_->GetBorderlessModeEnabled()) { |
| web_app_frame_toolbar_->SetVisible(false); |
| if (web_app_window_title_) { |
| web_app_window_title_->SetVisible(false); |
| } |
| return; |
| } |
| |
| gfx::Rect toolbar_bounds( |
| delegate_->GetBoundsForWebAppFrameToolbarInBrowserView()); |
| |
| web_app_frame_toolbar_->SetVisible(!toolbar_bounds.IsEmpty()); |
| if (web_app_window_title_) { |
| web_app_window_title_->SetVisible(!toolbar_bounds.IsEmpty()); |
| } |
| if (toolbar_bounds.IsEmpty()) { |
| return; |
| } |
| |
| if (delegate_->IsWindowControlsOverlayEnabled()) { |
| web_app_frame_toolbar_->LayoutForWindowControlsOverlay(toolbar_bounds); |
| toolbar_bounds.Subtract(web_app_frame_toolbar_->bounds()); |
| delegate_->UpdateWindowControlsOverlay(toolbar_bounds); |
| if (web_app_window_title_) { |
| web_app_window_title_->SetVisible(false); |
| } |
| return; |
| } |
| |
| gfx::Rect window_title_bounds = |
| web_app_frame_toolbar_->LayoutInContainer(toolbar_bounds); |
| |
| if (web_app_window_title_) { |
| if (delegate_->ShouldDrawTabStrip()) { |
| web_app_window_title_->SetVisible(false); |
| } else { |
| delegate_->LayoutWebAppWindowTitle(window_title_bounds, |
| *web_app_window_title_); |
| } |
| } |
| |
| available_bounds.set_y(toolbar_bounds.bottom()); |
| } |
| |
| void BrowserViewLayout::LayoutVerticalTabStrip(gfx::Rect& available_bounds) { |
| if (vertical_tab_strip_container_ && |
| vertical_tab_strip_container_->GetVisible()) { |
| vertical_tab_strip_container_->SetBounds( |
| available_bounds.x(), available_bounds.y(), |
| BrowserView::kVerticalTabStripWidth, available_bounds.height()); |
| available_bounds.set_x(available_bounds.x() + |
| BrowserView::kVerticalTabStripWidth); |
| } |
| } |
| |
| void BrowserViewLayout::LayoutTabStripRegion(gfx::Rect& available_bounds) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::LayoutTabStripRegion"); |
| if (!delegate_->ShouldDrawTabStrip()) { |
| SetViewVisibility(tab_strip_region_view_, false); |
| tab_strip_region_view_->SetBounds(0, 0, 0, 0); |
| return; |
| } |
| // This retrieves the bounds for the tab strip based on whether or not we show |
| // anything to the left of it, like the incognito avatar. |
| gfx::Rect tab_strip_region_bounds( |
| delegate_->GetBoundsForTabStripRegionInBrowserView()); |
| |
| if (web_app_frame_toolbar_) { |
| tab_strip_region_bounds.Inset(gfx::Insets::TLBR( |
| 0, 0, 0, web_app_frame_toolbar_->GetPreferredSize().width())); |
| } |
| |
| if (tabs::IsVerticalTabsFeatureEnabled() && ShouldDisplayVerticalTabs()) { |
| SetViewVisibility(tab_strip_region_view_, false); |
| } else { |
| SetViewVisibility(tab_strip_region_view_, true); |
| tab_strip_region_view_->SetBoundsRect(tab_strip_region_bounds); |
| available_bounds.set_y(tab_strip_region_bounds.bottom() - |
| GetLayoutConstant(TABSTRIP_TOOLBAR_OVERLAP)); |
| } |
| } |
| |
| void BrowserViewLayout::LayoutWebUITabStrip(gfx::Rect& available_bounds) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::LayoutWebUITabStrip"); |
| if (!webui_tab_strip_) { |
| return; |
| } |
| if (!webui_tab_strip_->GetVisible()) { |
| webui_tab_strip_->SetBoundsRect(gfx::Rect()); |
| return; |
| } |
| webui_tab_strip_->SetBounds( |
| available_bounds.x(), available_bounds.y(), available_bounds.width(), |
| webui_tab_strip_->GetHeightForWidth(available_bounds.width())); |
| available_bounds.set_y(webui_tab_strip_->bounds().bottom()); |
| } |
| |
| void BrowserViewLayout::LayoutToolbar(gfx::Rect& available_bounds) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::LayoutToolbar"); |
| bool toolbar_visible = delegate_->IsToolbarVisible(); |
| SetViewVisibility(toolbar_, toolbar_visible); |
| |
| if (tabs::IsVerticalTabsFeatureEnabled() && ShouldDisplayVerticalTabs()) { |
| gfx::Rect toolbar_bounds( |
| delegate_->GetBoundsForToolbarInVerticalTabBrowserView()); |
| toolbar_bounds.set_x(available_bounds.x()); |
| toolbar_bounds.set_width(toolbar_bounds.width() - |
| BrowserView::kVerticalTabStripWidth); |
| toolbar_->SetBoundsRect(toolbar_bounds); |
| } else { |
| int height = toolbar_visible ? toolbar_->GetPreferredSize().height() : 0; |
| int width = available_bounds.width(); |
| toolbar_->SetBounds(available_bounds.x(), available_bounds.y(), width, |
| height); |
| } |
| |
| SetClipPathWithBottomAllowance(toolbar_); |
| available_bounds.set_y(toolbar_->bounds().bottom()); |
| } |
| |
| void BrowserViewLayout::LayoutBookmarkAndInfoBars(gfx::Rect& available_bounds, |
| int browser_view_y) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::LayoutBookmarkAndInfoBars"); |
| dialog_top_y_ = |
| available_bounds.y() + browser_view_y - kConstrainedWindowOverlap; |
| |
| if (bookmark_bar_) { |
| available_bounds.set_y( |
| std::max(toolbar_->bounds().bottom(), available_bounds.y())); |
| LayoutBookmarkBar(available_bounds); |
| } |
| |
| if (delegate_->IsContentsSeparatorEnabled() && |
| (toolbar_->GetVisible() || bookmark_bar_) && available_bounds.y() > 0) { |
| int separator_height = 0; |
| if (multi_contents_view_) { |
| // Show top container separator when infobar is visible and for immersive |
| // full screen without always showing toolbar. |
| SetViewVisibility( |
| top_container_separator_, |
| IsInfobarVisible() || IsImmersiveModeEnabledWithoutToolbar()); |
| |
| if (top_container_separator_->GetVisible()) { |
| separator_height = |
| top_container_separator_->GetPreferredSize().height(); |
| top_container_separator_->SetBounds( |
| available_bounds.x(), available_bounds.y(), |
| available_bounds.width(), separator_height); |
| } |
| // If the loading bar will be shown, it's supposed to replace the |
| // separator in the content area. |
| multi_contents_view_->SetShouldShowTopSeparator( |
| !loading_bar_ && !top_container_separator_->GetVisible()); |
| } else { |
| separator_height = top_container_separator_->GetPreferredSize().height(); |
| SetViewVisibility(top_container_separator_, true); |
| top_container_separator_->SetBounds( |
| available_bounds.x(), available_bounds.y(), available_bounds.width(), |
| separator_height); |
| } |
| |
| if (loading_bar_) { |
| SetViewVisibility(loading_bar_, true); |
| loading_bar_->SetBounds(available_bounds.x(), available_bounds.y() - 2, |
| available_bounds.width(), separator_height + 2); |
| top_container_->ReorderChildView(loading_bar_, |
| top_container_->children().size()); |
| } |
| available_bounds.set_y(available_bounds.y() + separator_height); |
| } else { |
| SetViewVisibility(top_container_separator_, false); |
| if (multi_contents_view_) { |
| multi_contents_view_->SetShouldShowTopSeparator(false); |
| } |
| if (loading_bar_) { |
| SetViewVisibility(loading_bar_, false); |
| } |
| } |
| |
| LayoutInfoBar(available_bounds); |
| } |
| |
| void BrowserViewLayout::LayoutBookmarkBar(gfx::Rect& available_bounds) { |
| if (!delegate_->IsBookmarkBarVisible()) { |
| SetViewVisibility(bookmark_bar_, false); |
| // TODO(jamescook): Don't change the bookmark bar height when it is |
| // invisible, so we can use its height for layout even in that state. |
| bookmark_bar_->SetBounds(0, available_bounds.y(), browser_view_->width(), |
| 0); |
| return; |
| } |
| |
| bookmark_bar_->SetInfoBarVisible(IsInfobarVisible()); |
| int bookmark_bar_height = bookmark_bar_->GetPreferredSize().height(); |
| bookmark_bar_->SetBounds(available_bounds.x(), available_bounds.y(), |
| available_bounds.width(), bookmark_bar_height); |
| SetClipPathWithBottomAllowance(bookmark_bar_); |
| if (!features::IsPixelCanvasRecordingEnabled()) { |
| // Make sure the contents separator is painted last as the background for |
| // BookmarkVieBar/ToolbarView may paint over it otherwise. |
| // TODO(crbug.com/41344902): Remove once the pixel canvas is enabled on |
| // all aura platforms. |
| if (top_container_ == bookmark_bar_->parent()) { |
| top_container_->ReorderChildView(top_container_separator_, |
| top_container_->children().size()); |
| } |
| } |
| |
| // Set visibility after setting bounds, as the visibility update uses the |
| // bounds to determine if the mouse is hovering over a button. |
| SetViewVisibility(bookmark_bar_, true); |
| available_bounds.set_y(available_bounds.y() + bookmark_bar_height); |
| } |
| |
| void BrowserViewLayout::LayoutInfoBar(gfx::Rect& available_bounds) { |
| // In immersive fullscreen or when top-chrome is fully hidden due to the page |
| // gesture scroll slide behavior, the infobar always starts near the top of |
| // the screen. |
| const ImmersiveModeController* immersive_mode_controller = |
| delegate_->GetImmersiveModeController(); |
| int top = available_bounds.y(); |
| if (immersive_mode_controller->IsEnabled() || |
| (delegate_->IsTopControlsSlideBehaviorEnabled() && |
| delegate_->GetTopControlsSlideBehaviorShownRatio() == 0.f)) { |
| // Can be null in tests. |
| top = (browser_view_ ? browser_view_->y() : 0) + |
| immersive_mode_controller->GetMinimumContentOffset(); |
| } |
| // The content usually starts at the bottom of the infobar. When there is an |
| // extra infobar offset the infobar is shifted down while the content stays. |
| int infobar_top = top; |
| int content_top = infobar_top + infobar_container_->height(); |
| infobar_top += delegate_->GetExtraInfobarOffset(); |
| SetViewVisibility(infobar_container_, IsInfobarVisible()); |
| if (infobar_container_->GetVisible()) { |
| infobar_container_->SetBounds( |
| available_bounds.x(), infobar_top, available_bounds.width(), |
| infobar_container_->GetPreferredSize().height()); |
| } else { |
| infobar_container_->SetBounds(available_bounds.x(), infobar_top, 0, 0); |
| } |
| available_bounds.set_y(content_top); |
| } |
| |
| BrowserViewLayout::ContentsContainerLayoutResult |
| BrowserViewLayout::CalculateContentsContainerLayout( |
| const gfx::Rect& available_bounds) const { |
| gfx::Rect contents_container_bounds = available_bounds; |
| int vertical_tab_offset = 0; |
| if (tabs::IsVerticalTabsFeatureEnabled() && ShouldDisplayVerticalTabs()) { |
| vertical_tab_offset = BrowserView::kVerticalTabStripWidth; |
| contents_container_bounds.set_width(available_bounds.width() - |
| vertical_tab_offset); |
| } |
| |
| if (webui_tab_strip_ && webui_tab_strip_->GetVisible()) { |
| // The WebUI tab strip container should "push" the tab contents down without |
| // resizing it. |
| contents_container_bounds.Inset( |
| gfx::Insets().set_bottom(-webui_tab_strip_->size().height())); |
| } |
| |
| const bool side_panel_visible = |
| contents_height_side_panel_ && contents_height_side_panel_->GetVisible(); |
| if (!side_panel_visible) { |
| // The contents container takes all available space, and we're done. |
| return ContentsContainerLayoutResult{contents_container_bounds, |
| gfx::Rect(), |
| false, |
| false, |
| false, |
| gfx::Rect()}; |
| } |
| |
| SidePanel* side_panel = |
| views::AsViewClass<SidePanel>(contents_height_side_panel_); |
| |
| const bool side_panel_right_aligned = side_panel->IsRightAligned(); |
| views::View* side_panel_separator = |
| side_panel_right_aligned ? right_aligned_side_panel_separator_.get() |
| : left_aligned_side_panel_separator_.get(); |
| const int separator_width = |
| !side_panel_separator ? 0 |
| : side_panel_separator->GetPreferredSize().width(); |
| |
| // Side panel occupies some of the container's space. The side panel should |
| // never occupy more space than is available in the content window, and |
| // should never force the web contents to be smaller than its intended |
| // minimum. |
| gfx::Rect side_panel_bounds = contents_container_bounds; |
| |
| // If necessary, cap the side panel width at 2/3rds of the contents container |
| // width as long as the side panel remains at or above its minimum width. |
| if (side_panel->ShouldRestrictMaxWidth()) { |
| side_panel_bounds.set_width( |
| std::max(std::min(side_panel->GetPreferredSize().width(), |
| contents_container_bounds.width() * 2 / 3), |
| side_panel->GetMinimumSize().width())); |
| } else { |
| side_panel_bounds.set_width(std::min(side_panel->GetPreferredSize().width(), |
| contents_container_bounds.width() - |
| GetMinWebContentsWidth() - |
| separator_width)); |
| } |
| |
| double side_panel_visible_width = |
| side_panel_bounds.width() * |
| views::AsViewClass<SidePanel>(contents_height_side_panel_) |
| ->GetAnimationValue(); |
| |
| // Shrink container bounds to fit the side panel. |
| contents_container_bounds.set_width(contents_container_bounds.width() - |
| side_panel_visible_width - |
| separator_width); |
| |
| // In LTR, the point (0,0) represents the top left of the browser. |
| // In RTL, the point (0,0) represents the top right of the browser. |
| const bool contents_container_after_side_panel = |
| (base::i18n::IsRTL() && side_panel_right_aligned) || |
| (!base::i18n::IsRTL() && !side_panel_right_aligned); |
| |
| if (contents_container_after_side_panel) { |
| // When the side panel should appear before the main content area relative |
| // to the ui direction, move `contents_container_bounds` after the side |
| // panel. Also leave space for the separator. |
| contents_container_bounds.set_x(side_panel_visible_width + separator_width + |
| vertical_tab_offset); |
| side_panel_bounds.set_x(side_panel_bounds.x() - (side_panel_bounds.width() - |
| side_panel_visible_width)); |
| } else { |
| // When the side panel should appear after the main content area relative to |
| // the ui direction, move `side_panel_bounds` after the main content area. |
| // Also leave space for the separator. |
| side_panel_bounds.set_x(contents_container_bounds.right() + |
| separator_width); |
| } |
| |
| // Adjust the side panel separator bounds based on the side panel bounds |
| // calculated above. |
| gfx::Rect separator_bounds = side_panel_bounds; |
| // TODO (https://crbug.com/389972209): Adding 1px to the width as a bandaid |
| // fix. This covers a case with subpixeling where a thin line of the |
| // background finds its way to the front. |
| separator_bounds.set_width(separator_width + 1); |
| // If the side panel appears before `contents_container_bounds`, place the |
| // separator immediately after the side panel but before the container |
| // bounds. If the side panel appears after `contents_container_bounds`, |
| // place the separator immediately after the contents bounds but before the |
| // side panel. |
| separator_bounds.set_x(contents_container_after_side_panel |
| ? side_panel_bounds.right() |
| : contents_container_bounds.right()); |
| |
| return BrowserViewLayout::ContentsContainerLayoutResult{ |
| contents_container_bounds, |
| side_panel_bounds, |
| side_panel_visible, |
| side_panel_right_aligned, |
| contents_container_after_side_panel, |
| separator_bounds}; |
| } |
| |
| void BrowserViewLayout::LayoutContentsContainerView( |
| const gfx::Rect& available_bounds) { |
| TRACE_EVENT0("ui", "BrowserViewLayout::LayoutContentsContainerView"); |
| // |main_contents_region_| contains web page contents, side panel and |
| // devtools. See browser_view.h for details. |
| main_container_->SetBoundsRect(available_bounds); |
| |
| BrowserViewLayout::ContentsContainerLayoutResult layout_result = |
| CalculateContentsContainerLayout(main_container_->GetLocalBounds()); |
| contents_container_->SetBoundsRect(layout_result.contents_container_bounds); |
| |
| if (contents_height_side_panel_) { |
| contents_height_side_panel_->SetBoundsRect(layout_result.side_panel_bounds); |
| } |
| |
| if (multi_contents_view_) { |
| multi_contents_view_->SetShouldShowLeadingSeparator( |
| layout_result.side_panel_visible && |
| (layout_result.side_panel_right_aligned == base::i18n::IsRTL())); |
| |
| multi_contents_view_->SetShouldShowTrailingSeparator( |
| layout_result.side_panel_visible && |
| (layout_result.side_panel_right_aligned != base::i18n::IsRTL())); |
| } else { |
| SetViewVisibility(right_aligned_side_panel_separator_, |
| layout_result.side_panel_visible && |
| layout_result.side_panel_right_aligned); |
| right_aligned_side_panel_separator_->SetBoundsRect( |
| layout_result.separator_bounds); |
| SetViewVisibility(left_aligned_side_panel_separator_, |
| layout_result.side_panel_visible && |
| !layout_result.side_panel_right_aligned); |
| left_aligned_side_panel_separator_->SetBoundsRect( |
| layout_result.separator_bounds); |
| |
| SetViewVisibility(side_panel_rounded_corner_, |
| layout_result.side_panel_visible); |
| if (layout_result.side_panel_visible) { |
| // Adjust the rounded corner bounds based on the side panel bounds. |
| const int corner_size = |
| side_panel_rounded_corner_->GetPreferredSize().width(); |
| |
| const int top_separator_height = views::Separator::kThickness; |
| if (layout_result.contents_container_after_side_panel) { |
| side_panel_rounded_corner_->SetBounds( |
| layout_result.side_panel_bounds.right(), |
| layout_result.side_panel_bounds.y() - top_separator_height, |
| corner_size, corner_size); |
| } else { |
| side_panel_rounded_corner_->SetBounds( |
| layout_result.side_panel_bounds.x() - corner_size, |
| layout_result.side_panel_bounds.y() - top_separator_height, |
| corner_size, corner_size); |
| } |
| } |
| } |
| } |
| |
| void BrowserViewLayout::UpdateTopContainerBounds( |
| const gfx::Rect& available_bounds) { |
| // Set the bounds of the top container view such that it is tall enough to |
| // fully show all of its children. In particular, the bottom of the bookmark |
| // bar can be above the bottom of the toolbar while the bookmark bar is |
| // animating. The top container view is positioned relative to the top of the |
| // client view instead of relative to GetTopInsetInBrowserView() because the |
| // top container view paints parts of the frame (title, window controls) |
| // during an immersive fullscreen reveal. |
| int height = 0; |
| for (views::View* child : top_container_->children()) { |
| if (child->GetVisible()) { |
| height = std::max(height, child->bounds().bottom()); |
| } |
| } |
| |
| // Ensure that the top container view reaches the topmost view in the |
| // ClientView because the bounds of the top container view are used in |
| // layout and we assume that this is the case. |
| height = std::max(height, delegate_->GetTopInsetInBrowserView()); |
| |
| gfx::Rect top_container_bounds(available_bounds.width(), height); |
| |
| if (delegate_->IsTopControlsSlideBehaviorEnabled()) { |
| // If the top controls are fully hidden, then it's positioned outside the |
| // views' bounds. |
| const float ratio = delegate_->GetTopControlsSlideBehaviorShownRatio(); |
| top_container_bounds.set_y(ratio == 0 ? -height : 0); |
| } else { |
| // If the immersive mode controller is animating the top container, it may |
| // be partly offscreen. |
| top_container_bounds.set_y( |
| delegate_->GetImmersiveModeController()->GetTopContainerVerticalOffset( |
| top_container_bounds.size())); |
| } |
| top_container_->SetBoundsRect(top_container_bounds); |
| SetClipPathWithBottomAllowance(top_container_); |
| } |
| |
| int BrowserViewLayout::GetMinWebContentsWidth() const { |
| int min_width = |
| kMainBrowserContentsMinimumWidth - |
| contents_height_side_panel_->GetMinimumSize().width() - |
| (right_aligned_side_panel_separator_ |
| ? right_aligned_side_panel_separator_->GetPreferredSize().width() |
| : 0); |
| |
| // When in split view, the minimum width of the contents is higher. |
| if (multi_contents_view_) { |
| min_width = |
| std::max(min_width, 2 * multi_contents_view_->GetMinViewWidth()); |
| } |
| DCHECK_GE(min_width, 0); |
| return min_width; |
| } |
| |
| bool BrowserViewLayout::IsImmersiveModeEnabledWithoutToolbar() const { |
| return delegate_->GetImmersiveModeController()->IsEnabled() |
| #if BUILDFLAG(IS_MAC) |
| && (!fullscreen_utils::IsAlwaysShowToolbarEnabled( |
| browser_view_->browser()) || |
| fullscreen_utils::IsInContentFullscreen(browser_view_->browser())) |
| #endif |
| ; |
| } |
| |
| bool BrowserViewLayout::IsInfobarVisible() const { |
| return !infobar_container_->IsEmpty() && |
| (!browser_view_->IsFullscreen() || |
| !infobar_container_->ShouldHideInFullscreen()); |
| } |
| |
| void BrowserViewLayout::SetDelegateForTesting( |
| std::unique_ptr<BrowserViewLayoutDelegate> delegate) { |
| delegate_ = std::move(delegate); |
| browser_view_->InvalidateLayout(); |
| } |
| |
| bool BrowserViewLayout::ShouldDisplayVerticalTabs() const { |
| return browser_view_->browser() |
| ->browser_window_features() |
| ->vertical_tab_strip_state_controller() |
| ->ShouldDisplayVerticalTabs(); |
| } |