blob: 001e26249acf4f93b47dc6ddb669b3cb2865f741 [file] [log] [blame]
// Copyright 2025 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/contents_container_view.h"
#include <memory>
#include <optional>
#include "chrome/browser/actor/ui/actor_overlay_web_view.h"
#include "chrome/browser/devtools/devtools_contents_resizing_strategy.h"
#include "chrome/browser/enterprise/watermark/watermark_view.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/contents_capture_border_view.h"
#include "chrome/browser/ui/views/frame/contents_container_outline.h"
#include "chrome/browser/ui/views/frame/contents_separator.h"
#include "chrome/browser/ui/views/frame/contents_web_view.h"
#include "chrome/browser/ui/views/frame/multi_contents_view_mini_toolbar.h"
#include "chrome/browser/ui/views/frame/scrim_view.h"
#include "chrome/browser/ui/views/frame/tab_modal_dialog_host.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/new_tab_footer/footer_web_view.h"
#include "chrome/common/chrome_features.h"
#include "components/search/ntp_features.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/border.h"
#include "ui/views/layout/delegating_layout_manager.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/proposed_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/browser_ui/glic_border_view.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#endif
#if BUILDFLAG(IS_WIN)
#include "ui/views/widget/native_widget_aura.h"
#endif
namespace {
constexpr float kContentCornerRadius = 6;
constexpr gfx::RoundedCornersF kContentRoundedCorners{kContentCornerRadius};
constexpr int kSplitViewContentPadding = 4;
constexpr int kNewTabFooterSeparatorHeight = 1;
constexpr int kNewTabFooterHeight = 56;
} // namespace
ContentsContainerView::ContentsContainerView(BrowserView* browser_view)
: browser_view_(browser_view),
web_contents_modal_dialog_host_(browser_view_, this) {
SetLayoutManager(std::make_unique<views::DelegatingLayoutManager>(this));
SetProperty(views::kElementIdentifierKey, kContentsContainerViewElementId);
// The default z-order is the order in which children were added to the
// parent view. So first added devtools and the devtools scrim view (as it
// exists behind the content view), then the content view and new tab page
// footer. This should be followed by scrims, borders and lastly mini-toolbar.
auto devtools_web_view =
std::make_unique<views::WebView>(browser_view->GetProfile());
devtools_web_view->SetID(VIEW_ID_DEV_TOOLS_DOCKED);
devtools_web_view->SetVisible(false);
devtools_web_view_ = AddChildView(std::move(devtools_web_view));
devtools_scrim_view_ = AddChildView(std::make_unique<ScrimView>());
devtools_scrim_view_->layer()->SetName("DevtoolsScrimView");
contents_view_ = AddChildView(
std::make_unique<ContentsWebView>(browser_view->GetProfile()));
contents_view_->SetID(VIEW_ID_TAB_CONTAINER);
if (base::FeatureList::IsEnabled(ntp_features::kNtpFooter)) {
new_tab_footer_view_separator_ =
AddChildView(ContentsSeparator::CreateContentsSeparator());
new_tab_footer_view_separator_->SetVisible(false);
new_tab_footer_view_separator_->SetProperty(
views::kElementIdentifierKey, kFooterWebViewSeparatorElementId);
new_tab_footer_view_ =
AddChildView(std::make_unique<new_tab_footer::NewTabFooterWebView>(
browser_view->browser()));
new_tab_footer_view_->SetVisible(false);
}
watermark_view_ =
AddChildView(std::make_unique<enterprise_watermark::WatermarkView>());
contents_scrim_view_ = AddChildView(std::make_unique<ScrimView>());
contents_scrim_view_->layer()->SetName("ContentsScrimView");
if (features::kGlicActorUiOverlay.Get()) {
auto actor_overlay_web_view =
std::make_unique<ActorOverlayWebView>(browser_view->browser());
actor_overlay_web_view->SetID(VIEW_ID_ACTOR_OVERLAY);
actor_overlay_web_view->SetVisible(false);
actor_overlay_web_view_ = AddChildView(std::move(actor_overlay_web_view));
}
#if BUILDFLAG(ENABLE_GLIC)
if (glic::GlicEnabling::IsProfileEligible(browser_view->GetProfile())) {
glic_border_ =
AddChildView(views::Builder<glic::GlicBorderView>(
glic::GlicBorderView::Factory::Create(
browser_view->browser(), contents_view_))
.SetVisible(false)
.SetCanProcessEventsWithinSubtree(false)
.Build());
}
#endif
if (base::FeatureList::IsEnabled(features::kSideBySide)) {
mini_toolbar_ = AddChildView(std::make_unique<MultiContentsViewMiniToolbar>(
browser_view, contents_view_));
container_outline_ =
AddChildView(std::make_unique<ContentsContainerOutline>(mini_toolbar_));
}
view_bounds_observer_.Observe(contents_view_);
}
ContentsContainerView::~ContentsContainerView() = default;
std::vector<views::View*> ContentsContainerView::GetAccessiblePanes() {
std::vector<views::View*> accessible_panes;
if (contents_view_->GetVisible()) {
accessible_panes.push_back(contents_view_);
}
if (devtools_web_view_->GetVisible()) {
accessible_panes.push_back(devtools_web_view_);
}
if (devtools_scrim_view_->GetVisible()) {
accessible_panes.push_back(devtools_scrim_view_);
}
return accessible_panes;
}
void ContentsContainerView::UpdateBorderAndOverlay(bool is_in_split,
bool is_active,
bool is_highlighted) {
is_in_split_ = is_in_split;
// The border, mini toolbar, and scrim should not be visible if not in a
// split.
if (!is_in_split) {
SetBorder(nullptr);
ClearBorderRoundedCorners();
mini_toolbar_->SetVisible(false);
container_outline_->SetVisible(false);
return;
}
SetBorder(views::CreateEmptyBorder(gfx::Insets(
kSplitViewContentPadding + ContentsContainerOutline::kThickness)));
UpdateBorderRoundedCorners();
container_outline_->UpdateState(is_active, is_highlighted);
// Mini toolbar should only be visible for the inactive contents
// container view or both depending on configuration.
mini_toolbar_->UpdateState(is_active, is_highlighted);
}
void ContentsContainerView::UpdateBorderRoundedCorners() {
// Update devtools rounded corners. Note, devtools exists behind the contents
// view so all devtools corners are rounded.
devtools_web_view_->holder()->SetCornerRadii(kContentRoundedCorners);
devtools_scrim_view_->SetRoundedCorners(kContentRoundedCorners);
const bool devtools_in_upper_left =
devtools_web_view_->GetVisible() &&
current_devtools_docked_placement_ == DevToolsDockedPlacement::kLeft;
const bool devtools_in_upper_right =
devtools_web_view_->GetVisible() &&
current_devtools_docked_placement_ == DevToolsDockedPlacement::kRight;
const bool devtools_in_lower_left =
devtools_web_view_->GetVisible() &&
(current_devtools_docked_placement_ == DevToolsDockedPlacement::kBottom ||
current_devtools_docked_placement_ == DevToolsDockedPlacement::kLeft);
const bool devtools_in_lower_right =
devtools_web_view_->GetVisible() &&
(current_devtools_docked_placement_ == DevToolsDockedPlacement::kBottom ||
current_devtools_docked_placement_ == DevToolsDockedPlacement::kRight);
const gfx::RoundedCornersF content_upper_rounded_corners =
gfx::RoundedCornersF{devtools_in_upper_left ? 0 : kContentCornerRadius,
devtools_in_upper_right ? 0 : kContentCornerRadius,
0, 0};
const gfx::RoundedCornersF content_lower_rounded_corners =
gfx::RoundedCornersF{0, 0,
devtools_in_lower_right ? 0 : kContentCornerRadius,
devtools_in_lower_left ? 0 : kContentCornerRadius};
const gfx::RoundedCornersF content_rounded_corners =
gfx::RoundedCornersF{devtools_in_upper_left ? 0 : kContentCornerRadius,
devtools_in_upper_right ? 0 : kContentCornerRadius,
devtools_in_lower_right ? 0 : kContentCornerRadius,
devtools_in_lower_left ? 0 : kContentCornerRadius};
auto radii = new_tab_footer_view_ && new_tab_footer_view_->GetVisible()
? content_upper_rounded_corners
: content_rounded_corners;
contents_view_->SetBackgroundRadii(radii);
contents_view_->holder()->SetCornerRadii(radii);
contents_scrim_view_->SetRoundedCorners(kContentRoundedCorners);
if (new_tab_footer_view_) {
new_tab_footer_view_->holder()->SetCornerRadii(
content_lower_rounded_corners);
}
if (actor_overlay_web_view_) {
// ActorOverlayWebView should use the same radii as the contents view since
// it acts as a full transparent layer directly over the main web content.
actor_overlay_web_view_->holder()->SetCornerRadii(radii);
}
#if BUILDFLAG(ENABLE_GLIC)
if (glic_border_) {
glic_border_->SetRoundedCorners(content_rounded_corners);
}
#endif
}
void ContentsContainerView::ClearBorderRoundedCorners() {
constexpr gfx::RoundedCornersF kNoRoundedCorners = gfx::RoundedCornersF{0};
devtools_web_view_->holder()->SetCornerRadii(kNoRoundedCorners);
devtools_scrim_view_->SetRoundedCorners(kNoRoundedCorners);
contents_view_->SetBackgroundRadii(kNoRoundedCorners);
contents_view_->holder()->SetCornerRadii(kNoRoundedCorners);
if (new_tab_footer_view_) {
new_tab_footer_view_->holder()->SetCornerRadii(kNoRoundedCorners);
}
contents_scrim_view_->SetRoundedCorners(kNoRoundedCorners);
if (actor_overlay_web_view_) {
actor_overlay_web_view_->holder()->SetCornerRadii(kNoRoundedCorners);
}
#if BUILDFLAG(ENABLE_GLIC)
if (glic_border_) {
glic_border_->SetRoundedCorners(kNoRoundedCorners);
}
#endif
}
void ContentsContainerView::ChildVisibilityChanged(View* child) {
if ((child == new_tab_footer_view_ || child == devtools_web_view_) &&
is_in_split_) {
UpdateBorderRoundedCorners();
}
}
void ContentsContainerView::Layout(PassKey pass_key) {
LayoutSuperclass<views::View>(this);
if (capture_contents_border_widget_) {
UpdateCaptureContentsBorderLocation();
}
}
void ContentsContainerView::OnViewBoundsChanged(View* observed_view) {
if (observed_view == contents_view_) {
UpdateDevToolsDockedPlacement();
if (is_in_split_) {
UpdateBorderRoundedCorners();
}
}
}
void ContentsContainerView::SetContentsResizingStrategy(
const DevToolsContentsResizingStrategy& strategy) {
if (strategy_.Equals(strategy)) {
return;
}
strategy_.CopyFrom(strategy);
InvalidateLayout();
}
void ContentsContainerView::ApplyWatermarkSettings(
const std::string& watermark_text,
SkColor fill_color,
SkColor outline_color,
int font_size) {
watermark_view_->SetString(watermark_text, fill_color, outline_color,
font_size);
}
void ContentsContainerView::UpdateDevToolsDockedPlacement() {
DevToolsDockedPlacement placement = DevToolsDockedPlacement::kUnknown;
gfx::Rect contents_view_bounds = GetContentsViewBounds();
const gfx::Rect& container_bounds = GetContentsBounds();
// If contents_webview has the same bounds as webview_container, it either
// means that devtools are not open or devtools are open in a separate
// window (not docked).
if (contents_view_bounds == container_bounds) {
placement = DevToolsDockedPlacement::kNone;
} else if (contents_view_bounds.x() > container_bounds.x() &&
contents_view_bounds.y() == container_bounds.y() &&
contents_view_bounds.height() == container_bounds.height()) {
placement = DevToolsDockedPlacement::kLeft;
} else if (contents_view_bounds.origin() == container_bounds.origin() &&
contents_view_bounds.height() == container_bounds.height()) {
placement = DevToolsDockedPlacement::kRight;
} else if (contents_view_bounds.origin() == container_bounds.origin() &&
contents_view_bounds.width() == container_bounds.width()) {
placement = DevToolsDockedPlacement::kBottom;
}
// When browser window is resizing, the contents_container and web_contents
// bounds can be out of sync, resulting in a state, where it is impossible to
// infer docked placement based on contents webview bounds. In this case, use
// the last known docked placement, since resizing a window does not change
// the devtools dock placement.
if (placement != DevToolsDockedPlacement::kUnknown) {
current_devtools_docked_placement_ = placement;
}
}
void ContentsContainerView::ShowCaptureContentsBorder() {
if (!capture_contents_border_widget_) {
CreateCaptureContentsBorder();
}
UpdateCaptureContentsBorderLocation();
capture_contents_border_widget_->Show();
}
void ContentsContainerView::HideCaptureContentsBorder() {
if (capture_contents_border_widget_) {
capture_contents_border_widget_->Hide();
}
}
void ContentsContainerView::SetCaptureContentsBorderLocation(
std::optional<gfx::Rect> border_location) {
dynamic_capture_content_border_bounds_ = border_location;
if (capture_contents_border_widget_) {
UpdateCaptureContentsBorderLocation();
}
}
gfx::Rect ContentsContainerView::GetContentsViewBounds() const {
gfx::Rect contents_view_bounds = contents_view_->bounds();
if (new_tab_footer_view_ && new_tab_footer_view_->GetVisible()) {
CHECK(new_tab_footer_view_separator_);
contents_view_bounds.set_height(contents_view_bounds.height() +
new_tab_footer_view_->height() +
new_tab_footer_view_separator_->height());
}
return contents_view_bounds;
}
void ContentsContainerView::CreateCaptureContentsBorder() {
capture_contents_border_widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
views::Widget* widget = GetWidget();
params.parent = widget->GetNativeView();
params.context = widget->GetNativeWindow();
// Make the widget non-top level.
params.child = true;
params.name = "TabSharingContentsBorder";
params.remove_standard_frame = true;
// Let events go through to underlying view.
params.accept_events = false;
params.activatable = views::Widget::InitParams::Activatable::kNo;
#if BUILDFLAG(IS_WIN)
params.native_widget =
new views::NativeWidgetAura(capture_contents_border_widget_.get());
#endif // BUILDFLAG(IS_WIN)
capture_contents_border_widget_->Init(std::move(params));
auto contents_capture_border_view =
std::make_unique<ContentsCaptureBorderView>();
capture_contents_border_widget_->SetContentsView(
std::move(contents_capture_border_view));
capture_contents_border_widget_->SetVisibilityChangedAnimationsEnabled(false);
capture_contents_border_widget_->SetOpacity(0.50f);
}
void ContentsContainerView::UpdateCaptureContentsBorderLocation() {
gfx::Point contents_top_left;
#if BUILDFLAG(IS_CHROMEOS)
// On Ash placing the border widget on top of the contents container
// does not require an offset -- see crbug.com/1030925.
const gfx::Rect bounds_in_browser =
views::View::ConvertRectToTarget(this, browser_view_, GetLocalBounds());
contents_top_left = gfx::Point(bounds_in_browser.x(), bounds_in_browser.y());
#else
views::View::ConvertPointToScreen(this, &contents_top_left);
#endif
gfx::Rect rect;
if (dynamic_capture_content_border_bounds_) {
rect = gfx::Rect(
contents_top_left.x() + dynamic_capture_content_border_bounds_->x(),
contents_top_left.y() + dynamic_capture_content_border_bounds_->y(),
dynamic_capture_content_border_bounds_->width(),
dynamic_capture_content_border_bounds_->height());
} else {
rect = gfx::Rect(contents_top_left.x(), contents_top_left.y(), width(),
height());
}
#if BUILDFLAG(IS_CHROMEOS)
// Immersive top container might overlap with the blue border in fullscreen
// mode - see crbug.com/1392733. By insetting the bounds rectangle we ensure
// that the blue border is always placed below the top container.
if (ImmersiveModeController::From(browser_view_->browser())->IsRevealed()) {
const int delta =
browser_view_->top_container()->bounds().bottom() - rect.y();
if (delta > 0) {
rect.Inset(gfx::Insets().set_top(delta));
}
}
#endif
#if BUILDFLAG(IS_MAC)
// Zero sized widgets are not supported on mac.
if (rect.IsEmpty()) {
return;
}
#endif // BUILDFLAG(IS_MAC)
capture_contents_border_widget_->SetBounds(rect);
}
views::ProposedLayout ContentsContainerView::CalculateProposedLayout(
const views::SizeBounds& size_bounds) const {
views::ProposedLayout layouts;
if (!size_bounds.is_fully_bounded()) {
return layouts;
}
int height = size_bounds.height().value();
int width = size_bounds.width().value();
if (width == 0 || height == 0) {
// On Wayland we receive a resize to 0 width first before the actual
// size bounds. Ignore such requests.
return layouts;
}
gfx::Rect full_contents_bounds = GetContentsBounds();
gfx::Rect devtools_bounds;
// The area contents excluding devtools is drawn (ie |contents_view_|,
// |new_tab_footer_view_|, etc).
gfx::Rect non_devtools_contents_bounds;
ApplyDevToolsContentsResizingStrategy(strategy_, full_contents_bounds,
&devtools_bounds,
&non_devtools_contents_bounds);
gfx::Rect contents_view_bounds = non_devtools_contents_bounds;
// DevTools cares about the specific position, so we have to compensate RTL
// layout here.
layouts.child_layouts.emplace_back(
devtools_web_view_.get(), devtools_web_view_->GetVisible(),
GetMirroredRect(devtools_bounds),
views::SizeBounds(full_contents_bounds.size()));
layouts.child_layouts.emplace_back(
devtools_scrim_view_.get(), devtools_scrim_view_->GetVisible(),
GetMirroredRect(devtools_bounds),
views::SizeBounds(full_contents_bounds.size()));
if (new_tab_footer_view_) {
gfx::Rect footer_separator_rect, footer_rect;
if (new_tab_footer_view_->GetVisible()) {
// Shrink the rect for the contents view if the ntp footer is visible.
contents_view_bounds.set_height(non_devtools_contents_bounds.height() -
kNewTabFooterHeight -
kNewTabFooterSeparatorHeight);
footer_separator_rect =
gfx::Rect(contents_view_bounds.x(), contents_view_bounds.bottom(),
contents_view_bounds.width(), kNewTabFooterSeparatorHeight);
footer_rect =
gfx::Rect(contents_view_bounds.x(), contents_view_bounds.bottom(),
contents_view_bounds.width(), kNewTabFooterHeight);
}
layouts.child_layouts.emplace_back(new_tab_footer_view_separator_.get(),
new_tab_footer_view_->GetVisible(),
footer_separator_rect);
layouts.child_layouts.emplace_back(new_tab_footer_view_.get(),
new_tab_footer_view_->GetVisible(),
footer_rect);
}
const auto& contents_rect = GetMirroredRect(contents_view_bounds);
layouts.child_layouts.emplace_back(
contents_view_.get(), contents_view_->GetVisible(), contents_rect);
#if BUILDFLAG(ENABLE_GLIC)
if (glic_border_) {
// |glic_border_| should not be seen over devtools.
layouts.child_layouts.emplace_back(glic_border_.get(),
glic_border_->GetVisible(),
non_devtools_contents_bounds);
}
#endif
// The content scrim view should cover the entire contents bounds.
CHECK(contents_scrim_view_);
layouts.child_layouts.emplace_back(contents_scrim_view_.get(),
contents_scrim_view_->GetVisible(),
full_contents_bounds);
CHECK(watermark_view_);
layouts.child_layouts.emplace_back(watermark_view_.get(),
watermark_view_->GetVisible(),
full_contents_bounds);
// Actor Overlay view bounds are the same as the contents view.
if (actor_overlay_web_view_) {
layouts.child_layouts.emplace_back(
actor_overlay_web_view_.get(), actor_overlay_web_view_->GetVisible(),
non_devtools_contents_bounds, size_bounds);
}
if (mini_toolbar_) {
// |mini_toolbar_| should be offset in the bottom right corner, overlapping
// the outline. Shrink the available space by corner radius to ensure we
// have space to draw it at the corners.
views::SizeBounds available_space(width, height);
available_space.Enlarge(-ContentsContainerOutline::kCornerRadius,
-ContentsContainerOutline::kCornerRadius);
gfx::Size mini_toolbar_size =
mini_toolbar_->GetPreferredSize(available_space);
const int offset_x = width - mini_toolbar_size.width();
const int offset_y = height - mini_toolbar_size.height();
const gfx::Rect mini_toolbar_rect =
gfx::Rect(offset_x, offset_y, mini_toolbar_size.width(),
mini_toolbar_size.height());
layouts.child_layouts.emplace_back(
mini_toolbar_.get(), mini_toolbar_->GetVisible(), mini_toolbar_rect);
}
if (container_outline_) {
layouts.child_layouts.emplace_back(container_outline_.get(),
container_outline_->GetVisible(),
gfx::Rect(0, 0, width, height));
}
layouts.host_size = gfx::Size(width, height);
return layouts;
}
BEGIN_METADATA(ContentsContainerView)
END_METADATA