| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // 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/glass_browser_frame_view.h" |
| |
| #include <dwmapi.h> |
| #include <utility> |
| |
| #include "base/trace_event/common/trace_event_common.h" |
| #include "base/win/windows_version.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/app/chrome_dll_resource.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/extensions/hosted_app_browser_controller.h" |
| #include "chrome/browser/ui/view_ids.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/frame/hosted_app_button_container.h" |
| #include "chrome/browser/ui/views/frame/hosted_app_origin_text.h" |
| #include "chrome/browser/ui/views/profiles/profile_indicator_icon.h" |
| #include "chrome/browser/ui/views/tabs/new_tab_button.h" |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/browser/win/titlebar_config.h" |
| #include "content/public/browser/web_contents.h" |
| #include "skia/ext/image_operations.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/material_design/material_design_controller.h" |
| #include "ui/base/resource/resource_bundle_win.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/display/win/dpi.h" |
| #include "ui/display/win/screen_win.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/dip_util.h" |
| #include "ui/gfx/icon_util.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/scoped_canvas.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/resources/grit/views_resources.h" |
| #include "ui/views/win/hwnd_util.h" |
| #include "ui/views/window/client_view.h" |
| |
| using MD = ui::MaterialDesignController; |
| |
| HICON GlassBrowserFrameView::throbber_icons_[ |
| GlassBrowserFrameView::kThrobberIconCount]; |
| |
| using MD = ui::MaterialDesignController; |
| |
| namespace { |
| |
| // How far the profile switcher button is from the left of the minimize button. |
| constexpr int kProfileSwitcherButtonOffset = 1; |
| |
| // Converts the |image| to a Windows icon and returns the corresponding HICON |
| // handle. |image| is resized to desired |width| and |height| if needed. |
| base::win::ScopedHICON CreateHICONFromSkBitmapSizedTo( |
| const gfx::ImageSkia& image, |
| int width, |
| int height) { |
| return IconUtil::CreateHICONFromSkBitmap( |
| width == image.width() && height == image.height() |
| ? *image.bitmap() |
| : skia::ImageOperations::Resize(*image.bitmap(), |
| skia::ImageOperations::RESIZE_BEST, |
| width, height)); |
| } |
| |
| int AlignRight(views::View* view, |
| int next_leading_x, |
| int next_trailing_x, |
| int available_height) { |
| gfx::Size view_size; |
| if (view->visible()) |
| view_size = view->GetPreferredSize(); |
| const int width = std::min(view_size.width(), |
| std::max(0, next_trailing_x - next_leading_x)); |
| const int height = view_size.height(); |
| DCHECK_LE(height, available_height); |
| view->SetBounds(next_trailing_x - width, (available_height - height) / 2, |
| width, height); |
| return view->bounds().x(); |
| } |
| |
| } // namespace |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // GlassBrowserFrameView, public: |
| |
| constexpr char GlassBrowserFrameView::kClassName[]; |
| |
| GlassBrowserFrameView::GlassBrowserFrameView(BrowserFrame* frame, |
| BrowserView* browser_view) |
| : BrowserNonClientFrameView(frame, browser_view), |
| window_icon_(nullptr), |
| window_title_(nullptr), |
| minimize_button_(nullptr), |
| maximize_button_(nullptr), |
| restore_button_(nullptr), |
| close_button_(nullptr), |
| throbber_running_(false), |
| throbber_frame_(0) { |
| // We initialize all fields despite some of them being unused in some modes, |
| // since it's possible for modes to flip dynamically (e.g. if the user enables |
| // a high-contrast theme). Throbber icons are only used when ShowSystemIcon() |
| // is true. Everything else here is only used when |
| // ShouldCustomDrawSystemTitlebar() is true. |
| |
| if (browser_view->ShouldShowWindowIcon()) { |
| InitThrobberIcons(); |
| |
| window_icon_ = new TabIconView(this, nullptr); |
| window_icon_->set_is_light(true); |
| window_icon_->set_id(VIEW_ID_WINDOW_ICON); |
| // Stop the icon from intercepting clicks intended for the HTSYSMENU region |
| // of the window. Even though it does nothing on click, it will still |
| // prevent us from giving the event back to Windows to handle properly. |
| window_icon_->set_can_process_events_within_subtree(false); |
| AddChildView(window_icon_); |
| } |
| |
| if (browser_view->ShouldShowWindowTitle()) { |
| window_title_ = new views::Label(browser_view->GetWindowTitle()); |
| window_title_->SetSubpixelRenderingEnabled(false); |
| window_title_->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| window_title_->set_id(VIEW_ID_WINDOW_TITLE); |
| AddChildView(window_title_); |
| } |
| |
| if (extensions::HostedAppBrowserController::IsForExperimentalHostedAppBrowser( |
| browser_view->browser())) { |
| // TODO(alancutter): Avoid snapshotting GetTitlebarFeatureColor() values |
| // here and call it on demand in |
| // HostedAppButtonContainer::UpdateIconsColor() via a delegate interface. |
| SkColor active_color = GetTitlebarFeatureColor(true); |
| SkColor inactive_color = GetTitlebarFeatureColor(false); |
| hosted_app_origin_text_ = new HostedAppOriginText( |
| browser_view->browser(), active_color, inactive_color); |
| AddChildView(hosted_app_origin_text_); |
| hosted_app_button_container_ = new HostedAppButtonContainer( |
| frame, browser_view, hosted_app_origin_text_, active_color, |
| inactive_color); |
| AddChildView(hosted_app_button_container_); |
| } |
| |
| minimize_button_ = |
| CreateCaptionButton(VIEW_ID_MINIMIZE_BUTTON, IDS_APP_ACCNAME_MINIMIZE); |
| maximize_button_ = |
| CreateCaptionButton(VIEW_ID_MAXIMIZE_BUTTON, IDS_APP_ACCNAME_MAXIMIZE); |
| restore_button_ = |
| CreateCaptionButton(VIEW_ID_RESTORE_BUTTON, IDS_APP_ACCNAME_RESTORE); |
| close_button_ = |
| CreateCaptionButton(VIEW_ID_CLOSE_BUTTON, IDS_APP_ACCNAME_CLOSE); |
| } |
| |
| GlassBrowserFrameView::~GlassBrowserFrameView() { |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // GlassBrowserFrameView, BrowserNonClientFrameView implementation: |
| |
| bool GlassBrowserFrameView::CaptionButtonsOnLeadingEdge() const { |
| // Because we don't set WS_EX_LAYOUTRTL (which would conflict with Chrome's |
| // own RTL layout logic), Windows always draws the caption buttons on the |
| // right, even when we want to be RTL. See crbug.com/560619. |
| return !ShouldCustomDrawSystemTitlebar() && base::i18n::IsRTL(); |
| } |
| |
| gfx::Rect GlassBrowserFrameView::GetBoundsForTabStrip( |
| views::View* tabstrip) const { |
| const int x = GetTabStripLeftInset(); |
| int end_x = width() - ClientBorderThickness(false); |
| if (!CaptionButtonsOnLeadingEdge()) |
| end_x = std::min(MinimizeButtonX() - TabStripCaptionSpacing(), end_x); |
| return gfx::Rect(x, TopAreaHeight(false), std::max(0, end_x - x), |
| tabstrip->GetPreferredSize().height()); |
| } |
| |
| int GlassBrowserFrameView::GetTopInset(bool restored) const { |
| return GetClientAreaInsets(restored).top(); |
| } |
| |
| int GlassBrowserFrameView::GetThemeBackgroundXInset() const { |
| return 0; |
| } |
| |
| bool GlassBrowserFrameView::HasClientEdge() const { |
| // Native Windows 10 should never paint a client edge. |
| return base::win::GetVersion() < base::win::VERSION_WIN10 && |
| BrowserNonClientFrameView::HasClientEdge(); |
| } |
| |
| void GlassBrowserFrameView::UpdateThrobber(bool running) { |
| if (ShowCustomIcon()) |
| window_icon_->Update(); |
| |
| if (!ShowSystemIcon()) |
| return; |
| |
| if (throbber_running_) { |
| if (running) { |
| DisplayNextThrobberFrame(); |
| } else { |
| StopThrobber(); |
| } |
| } else if (running) { |
| StartThrobber(); |
| } |
| } |
| |
| gfx::Size GlassBrowserFrameView::GetMinimumSize() const { |
| gfx::Size min_size(browser_view()->GetMinimumSize()); |
| |
| // Account for the client area insets. |
| gfx::Insets insets = GetClientAreaInsets(false); |
| min_size.Enlarge(insets.width(), insets.height()); |
| // The content edge images have a shadow built into them. Client area insets |
| // do not include this shadow thickness. |
| constexpr int kContentEdgeShadowThickness = 2; |
| min_size.Enlarge(2 * kContentEdgeShadowThickness, 0); |
| |
| // Ensure that the minimum width is enough to hold a tab strip with minimum |
| // width at its usual insets. |
| if (browser_view()->IsTabStripVisible()) { |
| TabStrip* tabstrip = browser_view()->tabstrip(); |
| int min_tabstrip_width = tabstrip->GetMinimumSize().width(); |
| int min_tabstrip_area_width = |
| width() - GetBoundsForTabStrip(tabstrip).width() + min_tabstrip_width; |
| min_size.set_width(std::max(min_tabstrip_area_width, min_size.width())); |
| } |
| |
| return min_size; |
| } |
| |
| int GlassBrowserFrameView::GetTabStripLeftInset() const { |
| return incognito_bounds_.right() + GetTabstripPadding(); |
| } |
| |
| bool GlassBrowserFrameView::IsSingleTabModeAvailable() const { |
| // We can't paint the special single-tab appearance unless we're |
| // custom-drawing the titlebar. |
| return ShouldCustomDrawSystemTitlebar() && |
| BrowserNonClientFrameView::IsSingleTabModeAvailable(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // GlassBrowserFrameView, views::NonClientFrameView implementation: |
| |
| gfx::Rect GlassBrowserFrameView::GetBoundsForClientView() const { |
| return client_view_bounds_; |
| } |
| |
| gfx::Rect GlassBrowserFrameView::GetWindowBoundsForClientBounds( |
| const gfx::Rect& client_bounds) const { |
| HWND hwnd = views::HWNDForWidget(frame()); |
| if (!browser_view()->IsTabStripVisible() && hwnd) { |
| // If we don't have a tabstrip, we're either a popup or an app window, in |
| // which case we have a standard size non-client area and can just use |
| // AdjustWindowRectEx to obtain it. We check for a non-null window handle in |
| // case this gets called before the window is actually created. |
| RECT rect = client_bounds.ToRECT(); |
| AdjustWindowRectEx(&rect, GetWindowLong(hwnd, GWL_STYLE), FALSE, |
| GetWindowLong(hwnd, GWL_EXSTYLE)); |
| return gfx::Rect(rect); |
| } |
| |
| gfx::Insets insets = GetClientAreaInsets(false); |
| return gfx::Rect(std::max(0, client_bounds.x() - insets.left()), |
| std::max(0, client_bounds.y() - insets.top()), |
| client_bounds.width() + insets.width(), |
| client_bounds.height() + insets.height()); |
| } |
| |
| namespace { |
| |
| bool HitTestCaptionButton(Windows10CaptionButton* button, |
| const gfx::Point& point) { |
| return button && button->visible() && |
| button->GetMirroredBounds().Contains(point); |
| } |
| |
| } // namespace |
| |
| int GlassBrowserFrameView::NonClientHitTest(const gfx::Point& point) { |
| // For app windows and popups without a custom titlebar we haven't customized |
| // the frame at all so Windows can figure it out. |
| if (!ShouldCustomDrawSystemTitlebar() && |
| !browser_view()->IsBrowserTypeNormal()) |
| return HTNOWHERE; |
| |
| // If the point isn't within our bounds, then it's in the native portion of |
| // the frame so again Windows can figure it out. |
| if (!bounds().Contains(point)) |
| return HTNOWHERE; |
| |
| // See if the point is within the incognito icon or the profile switcher menu. |
| views::View* profile_switcher_view = GetProfileSwitcherButton(); |
| if ((profile_indicator_icon() && |
| profile_indicator_icon()->GetMirroredBounds().Contains(point)) || |
| (profile_switcher_view && |
| profile_switcher_view->GetMirroredBounds().Contains(point))) { |
| return HTCLIENT; |
| } |
| |
| int frame_component = frame()->client_view()->NonClientHitTest(point); |
| const int client_border_thickness = ClientBorderThickness(false); |
| |
| // See if we're in the sysmenu region. We still have to check the tabstrip |
| // first so that clicks in a tab don't get treated as sysmenu clicks. |
| if ((!MD::IsRefreshUi() || browser_view()->ShouldShowWindowIcon()) && |
| frame_component != HTCLIENT) { |
| gfx::Rect sys_menu_region( |
| client_border_thickness, |
| display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYSIZEFRAME), |
| display::win::ScreenWin::GetSystemMetricsInDIP(SM_CXSMICON), |
| display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYSMICON)); |
| if (sys_menu_region.Contains(point)) |
| return HTSYSMENU; |
| } |
| |
| if (frame_component != HTNOWHERE) |
| return frame_component; |
| |
| // Then see if the point is within any of the window controls. |
| if (HitTestCaptionButton(minimize_button_, point)) |
| return HTMINBUTTON; |
| if (HitTestCaptionButton(maximize_button_, point)) |
| return HTMAXBUTTON; |
| if (HitTestCaptionButton(restore_button_, point)) |
| return HTMAXBUTTON; |
| if (HitTestCaptionButton(close_button_, point)) |
| return HTCLOSE; |
| |
| // On Windows 8+, the caption buttons are almost butted up to the top right |
| // corner of the window. This code ensures the mouse isn't set to a size |
| // cursor while hovering over the caption buttons, thus giving the incorrect |
| // impression that the user can resize the window. |
| if (base::win::GetVersion() >= base::win::VERSION_WIN8) { |
| RECT button_bounds = {0}; |
| if (SUCCEEDED(DwmGetWindowAttribute(views::HWNDForWidget(frame()), |
| DWMWA_CAPTION_BUTTON_BOUNDS, |
| &button_bounds, |
| sizeof(button_bounds)))) { |
| gfx::Rect buttons = gfx::ConvertRectToDIP(display::win::GetDPIScale(), |
| gfx::Rect(button_bounds)); |
| |
| // There is a small one-pixel strip right above the caption buttons in |
| // which the resize border "peeks" through. |
| constexpr int kCaptionButtonTopInset = 1; |
| // The sizing region at the window edge above the caption buttons is |
| // 1 px regardless of scale factor. If we inset by 1 before converting |
| // to DIPs, the precision loss might eliminate this region entirely. The |
| // best we can do is to inset after conversion. This guarantees we'll |
| // show the resize cursor when resizing is possible. The cost of which |
| // is also maybe showing it over the portion of the DIP that isn't the |
| // outermost pixel. |
| buttons.Inset(0, kCaptionButtonTopInset, 0, 0); |
| if (buttons.Contains(point)) |
| return HTNOWHERE; |
| } |
| } |
| |
| int top_border_thickness = FrameTopBorderThickness(false); |
| // At the window corners the resize area is not actually bigger, but the 16 |
| // pixels at the end of the top and bottom edges trigger diagonal resizing. |
| constexpr int kResizeCornerWidth = 16; |
| // We want the resize corner behavior to apply to the kResizeCornerWidth |
| // pixels at each end of the top and bottom edges. Because |point|'s x |
| // coordinate is based on the DWM-inset portion of the window (so, it's 0 at |
| // the first pixel inside the left DWM margin), we need to subtract the DWM |
| // margin thickness, which we calculate as the total frame border thickness |
| // minus the nonclient border thickness. |
| const int dwm_margin = FrameBorderThickness() - client_border_thickness; |
| int window_component = GetHTComponentForFrame( |
| point, top_border_thickness, client_border_thickness, |
| top_border_thickness, kResizeCornerWidth - dwm_margin, |
| frame()->widget_delegate()->CanResize()); |
| // Fall back to the caption if no other component matches. |
| return (window_component == HTNOWHERE) ? HTCAPTION : window_component; |
| } |
| |
| void GlassBrowserFrameView::UpdateWindowIcon() { |
| if (ShowCustomIcon() && !frame()->IsFullscreen()) |
| window_icon_->SchedulePaint(); |
| } |
| |
| void GlassBrowserFrameView::UpdateWindowTitle() { |
| if (ShowCustomTitle() && !frame()->IsFullscreen()) { |
| LayoutTitleBar(); |
| window_title_->SchedulePaint(); |
| } |
| } |
| |
| void GlassBrowserFrameView::ResetWindowControls() { |
| minimize_button_->SetState(views::Button::STATE_NORMAL); |
| maximize_button_->SetState(views::Button::STATE_NORMAL); |
| restore_button_->SetState(views::Button::STATE_NORMAL); |
| close_button_->SetState(views::Button::STATE_NORMAL); |
| if (hosted_app_button_container_) |
| hosted_app_button_container_->UpdateContentSettingViewsVisibility(); |
| } |
| |
| void GlassBrowserFrameView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| if (sender == minimize_button_) |
| frame()->Minimize(); |
| else if (sender == maximize_button_) |
| frame()->Maximize(); |
| else if (sender == restore_button_) |
| frame()->Restore(); |
| else if (sender == close_button_) |
| frame()->Close(); |
| } |
| |
| bool GlassBrowserFrameView::ShouldTabIconViewAnimate() const { |
| DCHECK(ShowCustomIcon()); |
| const content::WebContents* current_tab = |
| browser_view()->GetActiveWebContents(); |
| return current_tab && current_tab->IsLoading(); |
| } |
| |
| gfx::ImageSkia GlassBrowserFrameView::GetFaviconForTabIconView() { |
| DCHECK(ShowCustomIcon()); |
| return frame()->widget_delegate()->GetWindowIcon(); |
| } |
| |
| void GlassBrowserFrameView::OnTabRemoved(int index) { |
| BrowserNonClientFrameView::OnTabRemoved(index); |
| // The profile switcher button may need to change height here, too. |
| // TabStripMaxXChanged is not enough when a tab other than the last tab is |
| // closed. |
| LayoutProfileSwitcher(); |
| } |
| |
| void GlassBrowserFrameView::OnTabsMaxXChanged() { |
| BrowserNonClientFrameView::OnTabsMaxXChanged(); |
| // The profile switcher button's height depends on the position of the new |
| // tab button, which may have changed if the tabs max X changed. |
| LayoutProfileSwitcher(); |
| } |
| |
| bool GlassBrowserFrameView::IsMaximized() const { |
| return frame()->IsMaximized(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // GlassBrowserFrameView, views::View overrides: |
| |
| const char* GlassBrowserFrameView::GetClassName() const { |
| return kClassName; |
| } |
| |
| void GlassBrowserFrameView::ChildPreferredSizeChanged(views::View* child) { |
| if (browser_view()->initialized() && child == hosted_app_button_container_) |
| Layout(); |
| } |
| |
| void GlassBrowserFrameView::OnPaint(gfx::Canvas* canvas) { |
| TRACE_EVENT0("views.frame", "GlassBrowserFrameView::OnPaint"); |
| if (ShouldCustomDrawSystemTitlebar()) |
| PaintTitlebar(canvas); |
| if (!browser_view()->IsTabStripVisible()) |
| return; |
| if (IsToolbarVisible()) |
| PaintToolbarTopStroke(canvas); |
| if (ClientBorderThickness(false) > 0) |
| PaintClientEdge(canvas); |
| } |
| |
| void GlassBrowserFrameView::Layout() { |
| TRACE_EVENT0("views.frame", "GlassBrowserFrameView::Layout"); |
| // The profile switcher and incognito icon depends on the caption button |
| // layout, so always call it first. |
| if (ShouldCustomDrawSystemTitlebar()) |
| LayoutCaptionButtons(); |
| |
| LayoutProfileSwitcher(); |
| |
| // The incognito area must be laid out even if we're not in incognito as |
| // tab-strip insets depend on it. When not in incognito the bounds will be |
| // zero-width but positioned correctly for the titlebar to start after it. |
| LayoutIncognitoIcon(); |
| |
| if (ShouldCustomDrawSystemTitlebar()) |
| LayoutTitleBar(); |
| |
| LayoutClientView(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // GlassBrowserFrameView, protected: |
| |
| // BrowserNonClientFrameView: |
| AvatarButtonStyle GlassBrowserFrameView::GetAvatarButtonStyle() const { |
| return AvatarButtonStyle::NATIVE; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // GlassBrowserFrameView, private: |
| |
| // views::NonClientFrameView: |
| bool GlassBrowserFrameView::DoesIntersectRect(const views::View* target, |
| const gfx::Rect& rect) const { |
| if (ShouldCustomDrawSystemTitlebar()) |
| return BrowserNonClientFrameView::DoesIntersectRect(target, rect); |
| |
| // TODO(bsep): This override has "dead zones" where you can't click on the |
| // custom titlebar buttons. It's not clear why it's necessary at all. |
| // Investigate tearing this out. |
| CHECK_EQ(target, this); |
| bool hit_incognito_icon = |
| profile_indicator_icon() && |
| profile_indicator_icon()->GetMirroredBounds().Intersects(rect); |
| views::View* profile_switcher_view = GetProfileSwitcherButton(); |
| bool hit_profile_switcher_button = |
| profile_switcher_view && |
| profile_switcher_view->GetMirroredBounds().Intersects(rect); |
| return hit_incognito_icon || hit_profile_switcher_button || |
| !frame()->client_view()->bounds().Intersects(rect); |
| } |
| |
| void GlassBrowserFrameView::ActivationChanged(bool active) { |
| BrowserNonClientFrameView::ActivationChanged(active); |
| |
| if (hosted_app_button_container_) { |
| hosted_app_button_container_->SetPaintAsActive(active); |
| hosted_app_origin_text_->SetPaintAsActive(active); |
| } |
| } |
| |
| int GlassBrowserFrameView::ClientBorderThickness(bool restored) const { |
| // The frame ends abruptly at the 1 pixel window border drawn by Windows 10. |
| if (!HasClientEdge()) |
| return 0; |
| |
| if ((IsMaximized() || frame()->IsFullscreen()) && !restored) |
| return 0; |
| |
| // Thickness of the frame edge between the non-client area and web content. |
| constexpr int kClientBorderThickness = 3; |
| return kClientBorderThickness; |
| } |
| |
| int GlassBrowserFrameView::FrameBorderThickness() const { |
| return (IsMaximized() || frame()->IsFullscreen()) |
| ? 0 |
| : display::win::ScreenWin::GetSystemMetricsInDIP(SM_CXSIZEFRAME); |
| } |
| |
| int GlassBrowserFrameView::FrameTopBorderThickness(bool restored) const { |
| // Under material refresh, the area above the tabstrip is much narrower. This |
| // code only returns this narrower value if in refresh mode and the frame is |
| // not maximized or fullscreen. When maximized, the OS sizes the window such |
| // that the border extends beyond the screen edges. In that case, we must |
| // return the default value. |
| if (MD::IsRefreshUi() && |
| ((!frame()->IsFullscreen() && !IsMaximized()) || restored)) { |
| constexpr int kTopResizeFrameArea = 5; |
| return kTopResizeFrameArea; |
| } |
| // Mouse and touch locations are floored but GetSystemMetricsInDIP is rounded, |
| // so we need to floor instead or else the difference will cause the hittest |
| // to fail when it ought to succeed. |
| // TODO(robliao): Resolve this GetSystemMetrics call. |
| return std::floor(FrameTopBorderThicknessPx(restored) / |
| display::win::GetDPIScale()); |
| } |
| |
| int GlassBrowserFrameView::FrameTopBorderThicknessPx(bool restored) const { |
| // Distinct from FrameBorderThickness() because Windows gives maximized |
| // windows an offscreen CYSIZEFRAME-thick region around the edges. The |
| // left/right/bottom edges don't worry about this because we cancel them out |
| // in BrowserDesktopWindowTreeHostWin::GetClientAreaInsets() so the offscreen |
| // area is non-client as far as Windows is concerned. However we can't do this |
| // with the top inset because otherwise Windows will give us a standard |
| // titlebar. Thus we must compensate here to avoid having UI elements drift |
| // off the top of the screen. |
| if (frame()->IsFullscreen() && !restored) |
| return 0; |
| return GetSystemMetrics(SM_CYSIZEFRAME); |
| } |
| |
| int GlassBrowserFrameView::TopAreaHeight(bool restored) const { |
| if (frame()->IsFullscreen() && !restored) |
| return 0; |
| |
| int top = FrameTopBorderThickness(restored); |
| if (!IsMaximized() || restored) { |
| // Besides the frame border, there's empty space atop the window in restored |
| // mode, to use to drag the window around. |
| constexpr int kNonClientRestoredExtraThickness = 11; |
| constexpr int kRefreshNonClientRestoredExtraThickness = 4; |
| top += MD::IsRefreshUi() ? kRefreshNonClientRestoredExtraThickness |
| : kNonClientRestoredExtraThickness; |
| } |
| return top; |
| } |
| |
| int GlassBrowserFrameView::TitlebarMaximizedVisualHeight() const { |
| return display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYCAPTION); |
| } |
| |
| int GlassBrowserFrameView::TitlebarHeight(bool restored) const { |
| if (frame()->IsFullscreen() && !restored) |
| return 0; |
| // The titlebar's actual height is the same in restored and maximized, but |
| // some of it is above the screen in maximized mode. See the comment in |
| // FrameTopBorderThicknessPx(). |
| return TitlebarMaximizedVisualHeight() + FrameTopBorderThickness(false); |
| } |
| |
| SkColor GlassBrowserFrameView::GetTitlebarFeatureColor(bool active) const { |
| const SkAlpha title_alpha = |
| active ? SK_AlphaOPAQUE : kInactiveTitlebarFeatureAlpha; |
| return SkColorSetA(color_utils::BlendTowardOppositeLuma(GetFrameColor(active), |
| SK_AlphaOPAQUE), |
| title_alpha); |
| } |
| |
| int GlassBrowserFrameView::WindowTopY() const { |
| // The window top is SM_CYSIZEFRAME pixels when maximized (see the comment in |
| // FrameTopBorderThickness()) and floor(system dsf) pixels when restored. |
| // Unfortunately we can't represent either of those at hidpi without using |
| // non-integral dips, so we return the closest reasonable values instead. |
| return IsMaximized() ? FrameTopBorderThickness(false) : 1; |
| } |
| |
| int GlassBrowserFrameView::MinimizeButtonX() const { |
| // When CaptionButtonsOnLeadingEdge() is true call |
| // frame()->GetMinimizeButtonOffset() directly, because minimize_button_->x() |
| // will give the wrong edge of the button. |
| DCHECK(!CaptionButtonsOnLeadingEdge()); |
| // If we're drawing the button we can query the layout directly, otherwise we |
| // need to ask Windows where the minimize button is. |
| // TODO(bsep): Ideally these would always be the same. When we're always |
| // custom drawing the caption buttons, remove GetMinimizeButtonOffset(). |
| return ShouldCustomDrawSystemTitlebar() ? minimize_button_->x() |
| : frame()->GetMinimizeButtonOffset(); |
| } |
| |
| int GlassBrowserFrameView::TabStripCaptionSpacing() const { |
| // In Refresh, any necessary padding after the tabstrip is contained within |
| // the tabs and/or new tab button. |
| if (MD::IsRefreshUi()) |
| return 0; |
| |
| // In restored mode, the New Tab button isn't at the same height as the |
| // caption buttons, but the space will look cluttered if it actually slides |
| // under them, so we stop it when the gap between the two is down to 5 px. |
| constexpr int kNewTabCaptionRestoredSpacing = 5; |
| // In maximized mode, where the New Tab button and the caption buttons are at |
| // similar vertical coordinates, we need to reserve a larger, 16 px gap to |
| // avoid looking too cluttered. |
| constexpr int kNewTabCaptionMaximizedSpacing = 16; |
| const int caption_spacing = IsMaximized() ? kNewTabCaptionMaximizedSpacing |
| : kNewTabCaptionRestoredSpacing; |
| |
| // The profile switcher button is optionally displayed to the left of the |
| // minimize button. |
| views::View* profile_switcher = GetProfileSwitcherButton(); |
| if (!profile_switcher) |
| return caption_spacing; |
| |
| int profile_spacing = |
| profile_switcher->width() + kProfileSwitcherButtonOffset; |
| |
| // In maximized mode, simply treat the profile switcher button as another |
| // caption button. |
| if (IsMaximized()) |
| return caption_spacing + profile_spacing; |
| |
| // When not maximized, allow the new tab button to slide completely under the |
| // the profile switcher button. |
| const auto* new_tab_button = browser_view()->tabstrip()->new_tab_button(); |
| profile_spacing -= new_tab_button->GetPreferredSize().width(); |
| |
| return std::max(caption_spacing, profile_spacing); |
| } |
| |
| bool GlassBrowserFrameView::IsToolbarVisible() const { |
| return browser_view()->IsToolbarVisible() && |
| !browser_view()->toolbar()->GetPreferredSize().IsEmpty(); |
| } |
| |
| bool GlassBrowserFrameView::ShowCustomIcon() const { |
| // Don't show the window icon when the incognito badge is visible, since |
| // they're competing for the same space. |
| // Hosted app windows don't include the window icon as per UI mocks. |
| return !profile_indicator_icon() && !hosted_app_button_container_ && |
| ShouldCustomDrawSystemTitlebar() && |
| browser_view()->ShouldShowWindowIcon(); |
| } |
| |
| bool GlassBrowserFrameView::ShowCustomTitle() const { |
| return ShouldCustomDrawSystemTitlebar() && |
| browser_view()->ShouldShowWindowTitle(); |
| } |
| |
| bool GlassBrowserFrameView::ShowSystemIcon() const { |
| return !ShouldCustomDrawSystemTitlebar() && |
| browser_view()->ShouldShowWindowIcon(); |
| } |
| |
| SkColor GlassBrowserFrameView::GetTitlebarColor() const { |
| return GetFrameColor(); |
| } |
| |
| HostedAppButtonContainer* |
| GlassBrowserFrameView::GetHostedAppButtonContainerForTesting() const { |
| return hosted_app_button_container_; |
| } |
| |
| Windows10CaptionButton* GlassBrowserFrameView::CreateCaptionButton( |
| ViewID button_type, |
| int accessible_name_resource_id) { |
| Windows10CaptionButton* button = new Windows10CaptionButton( |
| this, button_type, |
| l10n_util::GetStringUTF16(accessible_name_resource_id)); |
| AddChildView(button); |
| return button; |
| } |
| |
| void GlassBrowserFrameView::PaintTitlebar(gfx::Canvas* canvas) const { |
| TRACE_EVENT0("views.frame", "GlassBrowserFrameView::PaintTitlebar"); |
| gfx::Rect tabstrip_bounds = GetBoundsForTabStrip(browser_view()->tabstrip()); |
| |
| cc::PaintFlags flags; |
| gfx::ScopedCanvas scoped_canvas(canvas); |
| float scale = canvas->UndoDeviceScaleFactor(); |
| // This is the pixel-accurate version of WindowTopY(). Scaling the DIP values |
| // here compounds precision error, which exposes unpainted client area. When |
| // restored it uses the system dsf instead of the per-monitor dsf to match |
| // Windows' behavior. |
| const int y = IsMaximized() ? FrameTopBorderThicknessPx(false) |
| : std::floor(display::win::GetDPIScale()); |
| |
| // Draw the top of the accent border. |
| // |
| // We let the DWM do this for the other sides of the window by insetting the |
| // client area to leave nonclient area available. However, along the top |
| // window edge, we have to have zero nonclient area or the DWM will draw a |
| // full native titlebar outside our client area. See |
| // BrowserDesktopWindowTreeHostWin::GetClientAreaInsets(). |
| // |
| // We could ask the DWM to draw the top accent border in the client area (by |
| // calling DwmExtendFrameIntoClientArea() in |
| // BrowserDesktopWindowTreeHostWin::UpdateDWMFrame()), but this requires |
| // that we leave part of the client surface transparent. If we draw this |
| // ourselves, we can make the client surface fully opaque and avoid the |
| // power consumption needed for DWM to blend the window contents. |
| // |
| // So the accent border also has to be opaque, but native inactive borders |
| // are #494949 with 47% alpha. Against white (the most visible case) this is |
| // #AAAAAA, so we color with that normally. However, when the titlebar is dark |
| // that color sometimes stands out badly. In that case we lighten the titlebar |
| // color slightly, which creates a subtle highlight effect. This isn't exactly |
| // native but it looks good given our constraints. |
| const SkColor titlebar_color = GetTitlebarColor(); |
| const SkColor inactive_border_color = |
| color_utils::IsDark(titlebar_color) |
| ? color_utils::BlendTowardOppositeLuma(titlebar_color, 0x0F) |
| : SkColorSetRGB(0xAA, 0xAA, 0xAA); |
| flags.setColor( |
| ShouldPaintAsActive() |
| ? GetThemeProvider()->GetColor(ThemeProperties::COLOR_ACCENT_BORDER) |
| : inactive_border_color); |
| canvas->DrawRect(gfx::RectF(0, 0, width() * scale, y), flags); |
| |
| const gfx::Rect titlebar_rect = gfx::ToEnclosingRect( |
| gfx::RectF(0, y, width() * scale, tabstrip_bounds.bottom() * scale - y)); |
| // Paint the titlebar first so we have a background if an area isn't covered |
| // by the theme image. |
| flags.setColor(titlebar_color); |
| canvas->DrawRect(titlebar_rect, flags); |
| const gfx::ImageSkia frame_image = GetFrameImage(); |
| if (!frame_image.isNull()) { |
| canvas->TileImageInt(frame_image, 0, 0, titlebar_rect.x(), |
| titlebar_rect.y(), titlebar_rect.width(), |
| titlebar_rect.height(), scale); |
| } |
| const gfx::ImageSkia frame_overlay_image = GetFrameOverlayImage(); |
| if (!frame_overlay_image.isNull()) { |
| canvas->DrawImageInt(frame_overlay_image, 0, 0, frame_overlay_image.width(), |
| frame_overlay_image.height(), titlebar_rect.x(), |
| titlebar_rect.y(), frame_overlay_image.width() * scale, |
| frame_overlay_image.height() * scale, true); |
| } |
| |
| if (ShowCustomTitle()) { |
| window_title_->SetEnabledColor( |
| GetTitlebarFeatureColor(ShouldPaintAsActive())); |
| } |
| } |
| |
| void GlassBrowserFrameView::PaintClientEdge(gfx::Canvas* canvas) const { |
| // Draw the client edge images. |
| gfx::Rect client_bounds = CalculateClientAreaBounds(); |
| const int x = client_bounds.x(); |
| const int y = client_bounds.y() + browser_view()->GetToolbarBounds().y(); |
| const int right = client_bounds.right(); |
| const int bottom = std::max(y, height() - ClientBorderThickness(false)); |
| |
| const ui::ThemeProvider* tp = GetThemeProvider(); |
| if (base::win::GetVersion() < base::win::VERSION_WIN10) { |
| const gfx::ImageSkia* const right_image = |
| tp->GetImageSkiaNamed(IDR_CONTENT_RIGHT_SIDE); |
| const int img_w = right_image->width(); |
| const int height = bottom - y; |
| canvas->TileImageInt(*right_image, right, y, img_w, height); |
| canvas->DrawImageInt( |
| *tp->GetImageSkiaNamed(IDR_CONTENT_BOTTOM_RIGHT_CORNER), right, bottom); |
| const gfx::ImageSkia* const bottom_image = |
| tp->GetImageSkiaNamed(IDR_CONTENT_BOTTOM_CENTER); |
| canvas->TileImageInt(*bottom_image, x, bottom, client_bounds.width(), |
| bottom_image->height()); |
| canvas->DrawImageInt(*tp->GetImageSkiaNamed(IDR_CONTENT_BOTTOM_LEFT_CORNER), |
| x - img_w, bottom); |
| canvas->TileImageInt(*tp->GetImageSkiaNamed(IDR_CONTENT_LEFT_SIDE), |
| x - img_w, y, img_w, height); |
| } |
| FillClientEdgeRects(x, y, right, bottom, |
| tp->GetColor(ThemeProperties::COLOR_TOOLBAR), canvas); |
| } |
| |
| void GlassBrowserFrameView::FillClientEdgeRects(int x, |
| int y, |
| int right, |
| int bottom, |
| SkColor color, |
| gfx::Canvas* canvas) const { |
| gfx::Rect side(x - kClientEdgeThickness, y, kClientEdgeThickness, |
| bottom + kClientEdgeThickness - y); |
| canvas->FillRect(side, color); |
| canvas->FillRect(gfx::Rect(x, bottom, right - x, kClientEdgeThickness), |
| color); |
| side.set_x(right); |
| canvas->FillRect(side, color); |
| } |
| |
| void GlassBrowserFrameView::LayoutProfileSwitcher() { |
| if (!browser_view()->IsRegularOrGuestSession()) |
| return; |
| |
| View* profile_switcher = GetProfileSwitcherButton(); |
| if (!profile_switcher) |
| return; |
| |
| gfx::Size button_size = profile_switcher->GetPreferredSize(); |
| int button_width = button_size.width(); |
| int button_height = button_size.height(); |
| |
| int button_x; |
| if (CaptionButtonsOnLeadingEdge()) { |
| button_x = width() - frame()->GetMinimizeButtonOffset() + |
| kProfileSwitcherButtonOffset; |
| } else { |
| button_x = MinimizeButtonX() - kProfileSwitcherButtonOffset - button_width; |
| } |
| |
| int button_y = WindowTopY(); |
| if (IsMaximized()) { |
| // In maximized mode the caption buttons appear only 19 pixels high, but |
| // their contents are aligned as if they were 20 pixels high and extended |
| // 1 pixel off the top of the screen. We position the profile switcher |
| // button the same way to match. |
| button_y -= 1; |
| } |
| |
| // Shrink the button height when it's atop part of the tabstrip. In RTL the |
| // new tab button is on the left, so it can never slide under the avatar |
| // button, which is still on the right [http://crbug.com/560619]. |
| TabStrip* tabstrip = browser_view()->tabstrip(); |
| if (tabstrip && !CaptionButtonsOnLeadingEdge() && |
| (tabstrip->new_tab_button_bounds().right() > button_x)) |
| button_height = profile_switcher->GetMinimumSize().height(); |
| |
| profile_switcher->SetBounds(button_x, button_y, button_width, button_height); |
| } |
| |
| void GlassBrowserFrameView::LayoutIncognitoIcon() { |
| const gfx::Size size(GetIncognitoAvatarIcon().size()); |
| int x = ClientBorderThickness(false); |
| // In RTL, the icon needs to start after the caption buttons. |
| if (CaptionButtonsOnLeadingEdge()) { |
| x = width() - frame()->GetMinimizeButtonOffset() + |
| (GetProfileSwitcherButton() ? (GetProfileSwitcherButton()->width() + |
| kProfileSwitcherButtonOffset) |
| : 0); |
| } |
| const int bottom = GetTopInset(false) + browser_view()->GetTabStripHeight() - |
| GetAvatarIconPadding(); |
| incognito_bounds_.SetRect( |
| x + (profile_indicator_icon() ? GetAvatarIconPadding() : 0), |
| bottom - size.height(), profile_indicator_icon() ? size.width() : 0, |
| size.height()); |
| if (profile_indicator_icon()) |
| profile_indicator_icon()->SetBoundsRect(incognito_bounds_); |
| } |
| |
| void GlassBrowserFrameView::LayoutTitleBar() { |
| TRACE_EVENT0("views.frame", "GlassBrowserFrameView::LayoutTitleBar"); |
| if (!ShowCustomIcon() && !ShowCustomTitle()) |
| return; |
| |
| gfx::Rect window_icon_bounds; |
| const int icon_size = |
| display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYSMICON); |
| constexpr int kIconMaximizedLeftMargin = 2; |
| const int titlebar_visual_height = |
| IsMaximized() ? TitlebarMaximizedVisualHeight() : TitlebarHeight(false); |
| // Don't include the area above the screen when maximized. However it only |
| // looks centered if we start from y=0 when restored. |
| const int window_top = IsMaximized() ? WindowTopY() : 0; |
| int next_leading_x = |
| IsMaximized() |
| ? kIconMaximizedLeftMargin |
| : display::win::ScreenWin::GetSystemMetricsInDIP(SM_CXSIZEFRAME); |
| int next_trailing_x = MinimizeButtonX(); |
| |
| const int y = window_top + (titlebar_visual_height - icon_size) / 2; |
| window_icon_bounds = gfx::Rect(next_leading_x, y, icon_size, icon_size); |
| |
| constexpr int kIconTitleSpacing = 5; |
| if (ShowCustomIcon()) { |
| window_icon_->SetBoundsRect(window_icon_bounds); |
| next_leading_x = window_icon_bounds.right() + kIconTitleSpacing; |
| } else if (profile_indicator_icon()) { |
| next_leading_x = |
| profile_indicator_icon()->bounds().right() + kIconTitleSpacing; |
| } |
| |
| if (hosted_app_button_container_) { |
| DCHECK(!GetProfileSwitcherButton()); |
| next_trailing_x = AlignRight(hosted_app_button_container_, next_leading_x, |
| next_trailing_x, titlebar_visual_height); |
| hosted_app_button_container_->Layout(); |
| |
| next_trailing_x = AlignRight(hosted_app_origin_text_, next_leading_x, |
| next_trailing_x, titlebar_visual_height); |
| hosted_app_origin_text_->Layout(); |
| } |
| |
| if (ShowCustomTitle()) { |
| window_title_->SetText(browser_view()->GetWindowTitle()); |
| const int max_text_width = std::max(0, next_trailing_x - next_leading_x); |
| window_title_->SetBounds(next_leading_x, window_icon_bounds.y(), |
| max_text_width, window_icon_bounds.height()); |
| window_title_->SetAutoColorReadabilityEnabled(false); |
| } |
| } |
| |
| void GlassBrowserFrameView::LayoutCaptionButton(Windows10CaptionButton* button, |
| int previous_button_x) { |
| TRACE_EVENT0("views.frame", "GlassBrowserFrameView::LayoutCaptionButton"); |
| gfx::Size button_size = button->GetPreferredSize(); |
| button->SetBounds(previous_button_x - button_size.width(), WindowTopY(), |
| button_size.width(), button_size.height()); |
| } |
| |
| void GlassBrowserFrameView::LayoutCaptionButtons() { |
| TRACE_EVENT0("views.frame", "GlassBrowserFrameView::LayoutCaptionButtons"); |
| LayoutCaptionButton(close_button_, width()); |
| |
| LayoutCaptionButton(restore_button_, close_button_->x()); |
| restore_button_->SetVisible(IsMaximized()); |
| |
| LayoutCaptionButton(maximize_button_, close_button_->x()); |
| maximize_button_->SetVisible(!IsMaximized()); |
| |
| LayoutCaptionButton(minimize_button_, maximize_button_->x()); |
| } |
| |
| void GlassBrowserFrameView::LayoutClientView() { |
| client_view_bounds_ = CalculateClientAreaBounds(); |
| } |
| |
| gfx::Insets GlassBrowserFrameView::GetClientAreaInsets(bool restored) const { |
| if (!browser_view()->IsTabStripVisible()) { |
| const int top = |
| ShouldCustomDrawSystemTitlebar() ? TitlebarHeight(restored) : 0; |
| return gfx::Insets(top, 0, 0, 0); |
| } |
| |
| const int top_height = TopAreaHeight(restored); |
| const int border_thickness = ClientBorderThickness(restored); |
| return gfx::Insets(top_height, |
| border_thickness, |
| border_thickness, |
| border_thickness); |
| } |
| |
| gfx::Rect GlassBrowserFrameView::CalculateClientAreaBounds() const { |
| gfx::Rect bounds(GetLocalBounds()); |
| bounds.Inset(GetClientAreaInsets(false)); |
| return bounds; |
| } |
| |
| void GlassBrowserFrameView::StartThrobber() { |
| DCHECK(ShowSystemIcon()); |
| if (!throbber_running_) { |
| throbber_running_ = true; |
| throbber_frame_ = 0; |
| InitThrobberIcons(); |
| SendMessage(views::HWNDForWidget(frame()), WM_SETICON, |
| static_cast<WPARAM>(ICON_SMALL), |
| reinterpret_cast<LPARAM>(throbber_icons_[throbber_frame_])); |
| } |
| } |
| |
| void GlassBrowserFrameView::StopThrobber() { |
| DCHECK(ShowSystemIcon()); |
| if (throbber_running_) { |
| throbber_running_ = false; |
| |
| base::win::ScopedHICON previous_small_icon; |
| base::win::ScopedHICON previous_big_icon; |
| HICON small_icon = nullptr; |
| HICON big_icon = nullptr; |
| |
| gfx::ImageSkia icon = browser_view()->GetWindowIcon(); |
| if (!icon.isNull()) { |
| // Keep previous icons alive as long as they are referenced by the HWND. |
| previous_small_icon = std::move(small_window_icon_); |
| previous_big_icon = std::move(big_window_icon_); |
| |
| // Take responsibility for eventually destroying the created icons. |
| small_window_icon_ = CreateHICONFromSkBitmapSizedTo( |
| icon, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON)); |
| big_window_icon_ = CreateHICONFromSkBitmapSizedTo( |
| icon, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)); |
| |
| small_icon = small_window_icon_.get(); |
| big_icon = big_window_icon_.get(); |
| } |
| |
| // Fallback to class icon. |
| if (!small_icon) { |
| small_icon = reinterpret_cast<HICON>( |
| GetClassLongPtr(views::HWNDForWidget(frame()), GCLP_HICONSM)); |
| } |
| if (!big_icon) { |
| big_icon = reinterpret_cast<HICON>( |
| GetClassLongPtr(views::HWNDForWidget(frame()), GCLP_HICON)); |
| } |
| |
| // This will reset the icon which we set in the throbber code. |
| // WM_SETICON with null icon restores the icon for title bar but not |
| // for taskbar. See http://crbug.com/29996 |
| SendMessage(views::HWNDForWidget(frame()), WM_SETICON, |
| static_cast<WPARAM>(ICON_SMALL), |
| reinterpret_cast<LPARAM>(small_icon)); |
| |
| SendMessage(views::HWNDForWidget(frame()), WM_SETICON, |
| static_cast<WPARAM>(ICON_BIG), |
| reinterpret_cast<LPARAM>(big_icon)); |
| } |
| } |
| |
| void GlassBrowserFrameView::DisplayNextThrobberFrame() { |
| throbber_frame_ = (throbber_frame_ + 1) % kThrobberIconCount; |
| SendMessage(views::HWNDForWidget(frame()), WM_SETICON, |
| static_cast<WPARAM>(ICON_SMALL), |
| reinterpret_cast<LPARAM>(throbber_icons_[throbber_frame_])); |
| } |
| |
| // static |
| void GlassBrowserFrameView::InitThrobberIcons() { |
| static bool initialized = false; |
| if (!initialized) { |
| for (int i = 0; i < kThrobberIconCount; ++i) { |
| throbber_icons_[i] = |
| ui::LoadThemeIconFromResourcesDataDLL(IDI_THROBBER_01 + i); |
| DCHECK(throbber_icons_[i]); |
| } |
| initialized = true; |
| } |
| } |