Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(699)

Side by Side Diff: chrome/browser/ui/views/frame/glass_browser_frame_view.cc

Issue 2832823002: Update avatar button to MD (Closed)
Patch Set: Fixed ProfileChooserViewExtensionsTest browser tests Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/ui/views/frame/glass_browser_frame_view.h" 5 #include "chrome/browser/ui/views/frame/glass_browser_frame_view.h"
6 6
7 #include <dwmapi.h> 7 #include <dwmapi.h>
8 #include <utility> 8 #include <utility>
9 9
10 #include "base/win/windows_version.h" 10 #include "base/win/windows_version.h"
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
42 namespace { 42 namespace {
43 // Thickness of the frame edge between the non-client area and the web content. 43 // Thickness of the frame edge between the non-client area and the web content.
44 const int kClientBorderThickness = 3; 44 const int kClientBorderThickness = 3;
45 // Besides the frame border, there's empty space atop the window in restored 45 // Besides the frame border, there's empty space atop the window in restored
46 // mode, to use to drag the window around. 46 // mode, to use to drag the window around.
47 const int kNonClientRestoredExtraThickness = 11; 47 const int kNonClientRestoredExtraThickness = 11;
48 // At the window corners the resize area is not actually bigger, but the 16 48 // At the window corners the resize area is not actually bigger, but the 16
49 // pixels at the end of the top and bottom edges trigger diagonal resizing. 49 // pixels at the end of the top and bottom edges trigger diagonal resizing.
50 const int kResizeCornerWidth = 16; 50 const int kResizeCornerWidth = 16;
51 // How far the profile switcher button is from the left of the minimize button. 51 // How far the profile switcher button is from the left of the minimize button.
52 const int kProfileSwitcherButtonOffset = 5; 52 const int kProfileSwitcherButtonOffset = 1;
53 // The content edge images have a shadow built into them. 53 // The content edge images have a shadow built into them.
54 const int kContentEdgeShadowThickness = 2; 54 const int kContentEdgeShadowThickness = 2;
55 // In restored mode, the New Tab button isn't at the same height as the caption 55 // In restored mode, the New Tab button isn't at the same height as the caption
56 // buttons, but the space will look cluttered if it actually slides under them, 56 // buttons, but the space will look cluttered if it actually slides under them,
57 // so we stop it when the gap between the two is down to 5 px. 57 // so we stop it when the gap between the two is down to 5 px.
58 const int kNewTabCaptionRestoredSpacing = 5; 58 const int kNewTabCaptionRestoredSpacing = 5;
59 // In maximized mode, where the New Tab button and the caption buttons are at 59 // In maximized mode, where the New Tab button and the caption buttons are at
60 // similar vertical coordinates, we need to reserve a larger, 16 px gap to avoid 60 // similar vertical coordinates, we need to reserve a larger, 16 px gap to avoid
61 // looking too cluttered. 61 // looking too cluttered.
62 const int kNewTabCaptionMaximizedSpacing = 16; 62 const int kNewTabCaptionMaximizedSpacing = 16;
63 // Height of the profile switcher button. Same as the height of the Windows 7/8
64 // caption buttons.
65 // TODO(bsep): Windows 10 caption buttons look very different and we would like
66 // the profile switcher button to match on that platform.
67 const int kProfileSwitcherButtonHeight = 20;
68 // There is a small one-pixel strip right above the caption buttons in which the 63 // There is a small one-pixel strip right above the caption buttons in which the
69 // resize border "peeks" through. 64 // resize border "peeks" through.
70 const int kCaptionButtonTopInset = 1; 65 const int kCaptionButtonTopInset = 1;
71 66
72 // Converts the |image| to a Windows icon and returns the corresponding HICON 67 // Converts the |image| to a Windows icon and returns the corresponding HICON
73 // handle. |image| is resized to desired |width| and |height| if needed. 68 // handle. |image| is resized to desired |width| and |height| if needed.
74 base::win::ScopedHICON CreateHICONFromSkBitmapSizedTo( 69 base::win::ScopedHICON CreateHICONFromSkBitmapSizedTo(
75 const gfx::ImageSkia& image, 70 const gfx::ImageSkia& image,
76 int width, 71 int width,
77 int height) { 72 int height) {
(...skipping 14 matching lines...) Expand all
92 BrowserView* browser_view) 87 BrowserView* browser_view)
93 : BrowserNonClientFrameView(frame, browser_view), 88 : BrowserNonClientFrameView(frame, browser_view),
94 window_icon_(nullptr), 89 window_icon_(nullptr),
95 window_title_(nullptr), 90 window_title_(nullptr),
96 profile_switcher_(this), 91 profile_switcher_(this),
97 minimize_button_(nullptr), 92 minimize_button_(nullptr),
98 maximize_button_(nullptr), 93 maximize_button_(nullptr),
99 restore_button_(nullptr), 94 restore_button_(nullptr),
100 close_button_(nullptr), 95 close_button_(nullptr),
101 throbber_running_(false), 96 throbber_running_(false),
102 throbber_frame_(0) { 97 throbber_frame_(0),
98 tab_strip_(nullptr) {
103 // We initialize all fields despite some of them being unused in some modes, 99 // We initialize all fields despite some of them being unused in some modes,
104 // since it's possible for modes to flip dynamically (e.g. if the user enables 100 // since it's possible for modes to flip dynamically (e.g. if the user enables
105 // a high-contrast theme). Throbber icons are only used when ShowSystemIcon() 101 // a high-contrast theme). Throbber icons are only used when ShowSystemIcon()
106 // is true. Everything else here is only used when 102 // is true. Everything else here is only used when
107 // ShouldCustomDrawSystemTitlebar() is true. 103 // ShouldCustomDrawSystemTitlebar() is true.
108 104
109 if (browser_view->ShouldShowWindowIcon()) { 105 if (browser_view->ShouldShowWindowIcon()) {
110 InitThrobberIcons(); 106 InitThrobberIcons();
111 107
112 window_icon_ = new TabIconView(this, nullptr); 108 window_icon_ = new TabIconView(this, nullptr);
(...skipping 12 matching lines...) Expand all
125 AddChildView(window_title_); 121 AddChildView(window_title_);
126 } 122 }
127 123
128 minimize_button_ = CreateCaptionButton(VIEW_ID_MINIMIZE_BUTTON); 124 minimize_button_ = CreateCaptionButton(VIEW_ID_MINIMIZE_BUTTON);
129 maximize_button_ = CreateCaptionButton(VIEW_ID_MAXIMIZE_BUTTON); 125 maximize_button_ = CreateCaptionButton(VIEW_ID_MAXIMIZE_BUTTON);
130 restore_button_ = CreateCaptionButton(VIEW_ID_RESTORE_BUTTON); 126 restore_button_ = CreateCaptionButton(VIEW_ID_RESTORE_BUTTON);
131 close_button_ = CreateCaptionButton(VIEW_ID_CLOSE_BUTTON); 127 close_button_ = CreateCaptionButton(VIEW_ID_CLOSE_BUTTON);
132 } 128 }
133 129
134 GlassBrowserFrameView::~GlassBrowserFrameView() { 130 GlassBrowserFrameView::~GlassBrowserFrameView() {
131 if (tab_strip_) {
132 tab_strip_->RemoveObserver(this);
133 tab_strip_ = nullptr;
134 }
135 } 135 }
136 136
137 /////////////////////////////////////////////////////////////////////////////// 137 ///////////////////////////////////////////////////////////////////////////////
138 // GlassBrowserFrameView, BrowserNonClientFrameView implementation: 138 // GlassBrowserFrameView, BrowserNonClientFrameView implementation:
139 139
140 gfx::Rect GlassBrowserFrameView::GetBoundsForTabStrip( 140 gfx::Rect GlassBrowserFrameView::GetBoundsForTabStrip(
141 views::View* tabstrip) const { 141 views::View* tabstrip) const {
142 const int x = incognito_bounds_.right() + kAvatarIconPadding; 142 const int x = incognito_bounds_.right() + kAvatarIconPadding;
143 int end_x = width() - ClientBorderThickness(false); 143 int end_x = width() - ClientBorderThickness(false);
144 if (!CaptionButtonsOnLeadingEdge()) { 144 if (!CaptionButtonsOnLeadingEdge()) {
145 end_x = std::min(MinimizeButtonX(), end_x) - 145 end_x = std::min(MinimizeButtonX(), end_x) -
146 (IsMaximized() ? kNewTabCaptionMaximizedSpacing 146 (IsMaximized() ? kNewTabCaptionMaximizedSpacing
147 : kNewTabCaptionRestoredSpacing); 147 : kNewTabCaptionRestoredSpacing);
148 148
149 // The profile switcher button is optionally displayed to the left of the 149 // The profile switcher button is optionally displayed to the left of the
150 // minimize button. 150 // minimize button.
151 if (profile_switcher_.view()) { 151 views::View* button_view = GetProfileSwitcherButton();
152 if (button_view) {
152 const int old_end_x = end_x; 153 const int old_end_x = end_x;
153 end_x -= profile_switcher_.view()->width() + kProfileSwitcherButtonOffset; 154 end_x -= button_view->width() + kProfileSwitcherButtonOffset;
154 155
155 // In non-maximized mode, allow the new tab button to slide completely 156 // In non-maximized mode, allow the new tab button to slide completely
156 // under the profile switcher button. 157 // under the profile switcher button.
158 // Note that the button should be "cozy" then, even if it's an MD button.
157 if (!IsMaximized()) { 159 if (!IsMaximized()) {
158 end_x = std::min(end_x + GetLayoutSize(NEW_TAB_BUTTON).width() + 160 end_x = std::min(end_x + GetLayoutSize(NEW_TAB_BUTTON).width() +
159 kNewTabCaptionRestoredSpacing, 161 kNewTabCaptionRestoredSpacing,
160 old_end_x); 162 old_end_x);
161 } 163 }
162 } 164 }
163 } 165 }
164 return gfx::Rect(x, TopAreaHeight(false), std::max(0, end_x - x), 166 return gfx::Rect(x, TopAreaHeight(false), std::max(0, end_x - x),
165 tabstrip->GetPreferredSize().height()); 167 tabstrip->GetPreferredSize().height());
166 } 168 }
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
206 TabStrip* tabstrip = browser_view()->tabstrip(); 208 TabStrip* tabstrip = browser_view()->tabstrip();
207 int min_tabstrip_width = tabstrip->GetMinimumSize().width(); 209 int min_tabstrip_width = tabstrip->GetMinimumSize().width();
208 int min_tabstrip_area_width = 210 int min_tabstrip_area_width =
209 width() - GetBoundsForTabStrip(tabstrip).width() + min_tabstrip_width; 211 width() - GetBoundsForTabStrip(tabstrip).width() + min_tabstrip_width;
210 min_size.set_width(std::max(min_tabstrip_area_width, min_size.width())); 212 min_size.set_width(std::max(min_tabstrip_area_width, min_size.width()));
211 } 213 }
212 214
213 return min_size; 215 return min_size;
214 } 216 }
215 217
216 views::View* GlassBrowserFrameView::GetProfileSwitcherView() const { 218 views::MenuButton* GlassBrowserFrameView::GetProfileSwitcherButton() const {
217 return profile_switcher_.view(); 219 return profile_switcher_.button();
220 }
221
222 void GlassBrowserFrameView::OnBrowserViewInitViewsComplete() {
223 DCHECK(browser_view()->tabstrip());
224 DCHECK(!tab_strip_);
225 tab_strip_ = browser_view()->tabstrip();
226 tab_strip_->AddObserver(this);
218 } 227 }
219 228
220 /////////////////////////////////////////////////////////////////////////////// 229 ///////////////////////////////////////////////////////////////////////////////
221 // GlassBrowserFrameView, views::NonClientFrameView implementation: 230 // GlassBrowserFrameView, views::NonClientFrameView implementation:
222 231
223 gfx::Rect GlassBrowserFrameView::GetBoundsForClientView() const { 232 gfx::Rect GlassBrowserFrameView::GetBoundsForClientView() const {
224 return client_view_bounds_; 233 return client_view_bounds_;
225 } 234 }
226 235
227 gfx::Rect GlassBrowserFrameView::GetWindowBoundsForClientBounds( 236 gfx::Rect GlassBrowserFrameView::GetWindowBoundsForClientBounds(
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
263 return HTNOWHERE; 272 return HTNOWHERE;
264 273
265 // If the point isn't within our bounds, then it's in the native portion of 274 // If the point isn't within our bounds, then it's in the native portion of
266 // the frame so again Windows can figure it out. 275 // the frame so again Windows can figure it out.
267 if (!bounds().Contains(point)) 276 if (!bounds().Contains(point))
268 return HTNOWHERE; 277 return HTNOWHERE;
269 278
270 // See if the point is within the incognito icon or the profile switcher menu. 279 // See if the point is within the incognito icon or the profile switcher menu.
271 if ((profile_indicator_icon() && 280 if ((profile_indicator_icon() &&
272 profile_indicator_icon()->GetMirroredBounds().Contains(point)) || 281 profile_indicator_icon()->GetMirroredBounds().Contains(point)) ||
273 (profile_switcher_.view() && 282 (GetProfileSwitcherButton() &&
274 profile_switcher_.view()->GetMirroredBounds().Contains(point))) 283 GetProfileSwitcherButton()->GetMirroredBounds().Contains(point)))
275 return HTCLIENT; 284 return HTCLIENT;
276 285
277 int frame_component = frame()->client_view()->NonClientHitTest(point); 286 int frame_component = frame()->client_view()->NonClientHitTest(point);
278 287
279 // See if we're in the sysmenu region. We still have to check the tabstrip 288 // See if we're in the sysmenu region. We still have to check the tabstrip
280 // first so that clicks in a tab don't get treated as sysmenu clicks. 289 // first so that clicks in a tab don't get treated as sysmenu clicks.
281 int client_border_thickness = ClientBorderThickness(false); 290 int client_border_thickness = ClientBorderThickness(false);
282 gfx::Rect sys_menu_region( 291 gfx::Rect sys_menu_region(
283 client_border_thickness, 292 client_border_thickness,
284 display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYSIZEFRAME), 293 display::win::ScreenWin::GetSystemMetricsInDIP(SM_CYSIZEFRAME),
(...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after
427 return BrowserNonClientFrameView::DoesIntersectRect(target, rect); 436 return BrowserNonClientFrameView::DoesIntersectRect(target, rect);
428 437
429 // TODO(bsep): This override has "dead zones" where you can't click on the 438 // TODO(bsep): This override has "dead zones" where you can't click on the
430 // custom titlebar buttons. It's not clear why it's necessary at all. 439 // custom titlebar buttons. It's not clear why it's necessary at all.
431 // Investigate tearing this out. 440 // Investigate tearing this out.
432 CHECK_EQ(target, this); 441 CHECK_EQ(target, this);
433 bool hit_incognito_icon = 442 bool hit_incognito_icon =
434 profile_indicator_icon() && 443 profile_indicator_icon() &&
435 profile_indicator_icon()->GetMirroredBounds().Intersects(rect); 444 profile_indicator_icon()->GetMirroredBounds().Intersects(rect);
436 bool hit_profile_switcher_button = 445 bool hit_profile_switcher_button =
437 profile_switcher_.view() && 446 GetProfileSwitcherButton() &&
438 profile_switcher_.view()->GetMirroredBounds().Intersects(rect); 447 GetProfileSwitcherButton()->GetMirroredBounds().Intersects(rect);
439 return hit_incognito_icon || hit_profile_switcher_button || 448 return hit_incognito_icon || hit_profile_switcher_button ||
440 !frame()->client_view()->bounds().Intersects(rect); 449 !frame()->client_view()->bounds().Intersects(rect);
441 } 450 }
442 451
443 int GlassBrowserFrameView::ClientBorderThickness(bool restored) const { 452 int GlassBrowserFrameView::ClientBorderThickness(bool restored) const {
444 // The frame ends abruptly at the 1 pixel window border drawn by Windows 10. 453 // The frame ends abruptly at the 1 pixel window border drawn by Windows 10.
445 if (!browser_view()->HasClientEdge()) 454 if (!browser_view()->HasClientEdge())
446 return 0; 455 return 0;
447 456
448 if ((IsMaximized() || frame()->IsFullscreen()) && !restored) 457 if ((IsMaximized() || frame()->IsFullscreen()) && !restored)
(...skipping 259 matching lines...) Expand 10 before | Expand all | Expand 10 after
708 gfx::Canvas* canvas) const { 717 gfx::Canvas* canvas) const {
709 gfx::Rect side(x - kClientEdgeThickness, y, kClientEdgeThickness, 718 gfx::Rect side(x - kClientEdgeThickness, y, kClientEdgeThickness,
710 bottom + kClientEdgeThickness - y); 719 bottom + kClientEdgeThickness - y);
711 canvas->FillRect(side, color); 720 canvas->FillRect(side, color);
712 canvas->FillRect(gfx::Rect(x, bottom, right - x, kClientEdgeThickness), 721 canvas->FillRect(gfx::Rect(x, bottom, right - x, kClientEdgeThickness),
713 color); 722 color);
714 side.set_x(right); 723 side.set_x(right);
715 canvas->FillRect(side, color); 724 canvas->FillRect(side, color);
716 } 725 }
717 726
727 void GlassBrowserFrameView::TabStripMaxXChanged(TabStrip* tab_strip) {
728 // May switch between cozy and tall MD avatar button here
msarda 2017/04/21 09:43:28 Add a "." at the end of the comment.
emx 2017/04/24 16:23:10 Done.
729 profile_switcher_.ButtonPreferredSizeChanged();
730 }
731
732 void GlassBrowserFrameView::TabStripRemovedTabAt(TabStrip* tab_strip,
733 int index) {
734 // May switch between cozy and tall button here, too. TabStripMaxXChanged
735 // is not enough when a tab other than the last tab is closed.
736 profile_switcher_.ButtonPreferredSizeChanged();
737 }
738
739 void GlassBrowserFrameView::TabStripDeleted(TabStrip* tab_strip) {
msarda 2017/04/21 09:43:28 Maybe check that the tab strip pointers match: DCH
emx 2017/04/24 16:23:10 Done.
740 DCHECK(tab_strip_);
741 tab_strip_->RemoveObserver(this);
742 tab_strip_ = nullptr;
743 }
744
718 void GlassBrowserFrameView::LayoutProfileSwitcher() { 745 void GlassBrowserFrameView::LayoutProfileSwitcher() {
719 DCHECK(browser_view()->IsRegularOrGuestSession()); 746 DCHECK(browser_view()->IsRegularOrGuestSession());
720 if (!profile_switcher_.view()) 747 if (!GetProfileSwitcherButton())
msarda 2017/04/21 09:43:28 Instead of caling GetProfileSwitcherButton() 4 tim
emx 2017/04/24 16:23:10 Done.
721 return; 748 return;
722 749
723 gfx::Size label_size = profile_switcher_.view()->GetPreferredSize(); 750 gfx::Size button_size = GetProfileSwitcherButton()->GetPreferredSize();
751 int button_width = button_size.width();
752 int button_height = button_size.height();
724 753
725 int button_x; 754 int button_x;
726 if (CaptionButtonsOnLeadingEdge()) { 755 if (CaptionButtonsOnLeadingEdge()) {
727 button_x = width() - frame()->GetMinimizeButtonOffset() + 756 button_x = width() - frame()->GetMinimizeButtonOffset() +
728 kProfileSwitcherButtonOffset; 757 kProfileSwitcherButtonOffset;
729 } else { 758 } else {
730 button_x = 759 button_x = MinimizeButtonX() - kProfileSwitcherButtonOffset - button_width;
731 MinimizeButtonX() - kProfileSwitcherButtonOffset - label_size.width();
732 } 760 }
733 761
734 int button_y = WindowTopY(); 762 int button_y = WindowTopY();
763
msarda 2017/04/21 09:43:28 Revert the add of this new line.
emx 2017/04/24 16:23:10 Done.
735 if (IsMaximized()) { 764 if (IsMaximized()) {
736 // In maximized mode the caption buttons appear only 19 pixels high, but 765 // In maximized mode the caption buttons appear only 19 pixels high, but
737 // their contents are aligned as if they were 20 pixels high and extended 766 // their contents are aligned as if they were 20 pixels high and extended
738 // 1 pixel off the top of the screen. We position the profile switcher 767 // 1 pixel off the top of the screen. We position the profile switcher
739 // button the same way to match. 768 // button the same way to match.
740 button_y -= 1; 769 button_y -= 1;
741 } 770 }
742 profile_switcher_.view()->SetBounds(button_x, button_y, label_size.width(), 771
743 kProfileSwitcherButtonHeight); 772 profile_switcher_.button()->UpdateButtonHeightForPosition(button_x,
msarda 2017/04/21 09:43:28 Should this be: GetProfileSwitcherButton()?
emx 2017/04/24 16:23:10 No, that returns MenuButton* and we need an Avatar
773 &button_height);
774 GetProfileSwitcherButton()->SetBounds(button_x, button_y, button_width,
775 button_height);
744 } 776 }
745 777
746 void GlassBrowserFrameView::LayoutIncognitoIcon() { 778 void GlassBrowserFrameView::LayoutIncognitoIcon() {
747 const gfx::Size size(GetIncognitoAvatarIcon().size()); 779 const gfx::Size size(GetIncognitoAvatarIcon().size());
748 int x = ClientBorderThickness(false); 780 int x = ClientBorderThickness(false);
749 // In RTL, the icon needs to start after the caption buttons. 781 // In RTL, the icon needs to start after the caption buttons.
750 if (CaptionButtonsOnLeadingEdge()) { 782 if (CaptionButtonsOnLeadingEdge()) {
751 x = width() - frame()->GetMinimizeButtonOffset() + 783 x = width() - frame()->GetMinimizeButtonOffset() +
752 (profile_switcher_.view() ? (profile_switcher_.view()->width() + 784 (GetProfileSwitcherButton() ? (GetProfileSwitcherButton()->width() +
753 kProfileSwitcherButtonOffset) 785 kProfileSwitcherButtonOffset)
754 : 0); 786 : 0);
755 } 787 }
756 const int bottom = GetTopInset(false) + browser_view()->GetTabStripHeight() - 788 const int bottom = GetTopInset(false) + browser_view()->GetTabStripHeight() -
757 kAvatarIconPadding; 789 kAvatarIconPadding;
758 incognito_bounds_.SetRect( 790 incognito_bounds_.SetRect(
759 x + (profile_indicator_icon() ? kAvatarIconPadding : 0), 791 x + (profile_indicator_icon() ? kAvatarIconPadding : 0),
760 bottom - size.height(), profile_indicator_icon() ? size.width() : 0, 792 bottom - size.height(), profile_indicator_icon() ? size.width() : 0,
761 size.height()); 793 size.height());
762 if (profile_indicator_icon()) 794 if (profile_indicator_icon())
763 profile_indicator_icon()->SetBoundsRect(incognito_bounds_); 795 profile_indicator_icon()->SetBoundsRect(incognito_bounds_);
764 } 796 }
(...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after
915 static bool initialized = false; 947 static bool initialized = false;
916 if (!initialized) { 948 if (!initialized) {
917 for (int i = 0; i < kThrobberIconCount; ++i) { 949 for (int i = 0; i < kThrobberIconCount; ++i) {
918 throbber_icons_[i] = 950 throbber_icons_[i] =
919 ui::LoadThemeIconFromResourcesDataDLL(IDI_THROBBER_01 + i); 951 ui::LoadThemeIconFromResourcesDataDLL(IDI_THROBBER_01 + i);
920 DCHECK(throbber_icons_[i]); 952 DCHECK(throbber_icons_[i]);
921 } 953 }
922 initialized = true; 954 initialized = true;
923 } 955 }
924 } 956 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698