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

Side by Side Diff: mandoline/ui/desktop_ui/browser_window.cc

Issue 1326443006: mandoline: Add back/forward support and UI. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: nits and such. Created 5 years, 3 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
« no previous file with comments | « mandoline/ui/desktop_ui/browser_window.h ('k') | mandoline/ui/desktop_ui/toolbar_view.h » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 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 "mandoline/ui/desktop_ui/browser_window.h" 5 #include "mandoline/ui/desktop_ui/browser_window.h"
6 6
7 #include "base/command_line.h" 7 #include "base/command_line.h"
8 #include "base/strings/string16.h" 8 #include "base/strings/string16.h"
9 #include "base/strings/utf_string_conversions.h" 9 #include "base/strings/utf_string_conversions.h"
10 #include "base/time/time.h" 10 #include "base/time/time.h"
11 #include "components/view_manager/public/cpp/scoped_view_ptr.h" 11 #include "components/view_manager/public/cpp/scoped_view_ptr.h"
12 #include "components/view_manager/public/cpp/view_tree_host_factory.h" 12 #include "components/view_manager/public/cpp/view_tree_host_factory.h"
13 #include "mandoline/ui/aura/native_widget_view_manager.h" 13 #include "mandoline/ui/aura/native_widget_view_manager.h"
14 #include "mandoline/ui/desktop_ui/browser_commands.h" 14 #include "mandoline/ui/desktop_ui/browser_commands.h"
15 #include "mandoline/ui/desktop_ui/browser_manager.h" 15 #include "mandoline/ui/desktop_ui/browser_manager.h"
16 #include "mandoline/ui/desktop_ui/public/interfaces/omnibox.mojom.h" 16 #include "mandoline/ui/desktop_ui/public/interfaces/omnibox.mojom.h"
17 #include "mandoline/ui/desktop_ui/toolbar_view.h"
17 #include "mojo/common/common_type_converters.h" 18 #include "mojo/common/common_type_converters.h"
18 #include "mojo/converters/geometry/geometry_type_converters.h" 19 #include "mojo/converters/geometry/geometry_type_converters.h"
19 #include "mojo/services/tracing/public/cpp/switches.h" 20 #include "mojo/services/tracing/public/cpp/switches.h"
20 #include "mojo/services/tracing/public/interfaces/tracing.mojom.h" 21 #include "mojo/services/tracing/public/interfaces/tracing.mojom.h"
21 #include "ui/gfx/canvas.h" 22 #include "ui/gfx/canvas.h"
22 #include "ui/views/background.h" 23 #include "ui/views/background.h"
23 #include "ui/views/controls/button/label_button.h" 24 #include "ui/views/controls/button/label_button.h"
24 #include "ui/views/widget/widget_delegate.h" 25 #include "ui/views/widget/widget_delegate.h"
25 26
26 namespace mandoline { 27 namespace mandoline {
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
64 65
65 //////////////////////////////////////////////////////////////////////////////// 66 ////////////////////////////////////////////////////////////////////////////////
66 // BrowserWindow, public: 67 // BrowserWindow, public:
67 68
68 BrowserWindow::BrowserWindow(mojo::ApplicationImpl* app, 69 BrowserWindow::BrowserWindow(mojo::ApplicationImpl* app,
69 mojo::ViewTreeHostFactory* host_factory, 70 mojo::ViewTreeHostFactory* host_factory,
70 BrowserManager* manager) 71 BrowserManager* manager)
71 : app_(app), 72 : app_(app),
72 host_client_binding_(this), 73 host_client_binding_(this),
73 manager_(manager), 74 manager_(manager),
74 omnibox_launcher_(nullptr), 75 toolbar_view_(nullptr),
75 progress_bar_(nullptr), 76 progress_bar_(nullptr),
76 root_(nullptr), 77 root_(nullptr),
77 content_(nullptr), 78 content_(nullptr),
78 omnibox_view_(nullptr), 79 omnibox_view_(nullptr),
79 web_view_(this) { 80 web_view_(this) {
80 mojo::ViewTreeHostClientPtr host_client; 81 mojo::ViewTreeHostClientPtr host_client;
81 host_client_binding_.Bind(GetProxy(&host_client)); 82 host_client_binding_.Bind(GetProxy(&host_client));
82 mojo::CreateViewTreeHost(host_factory, host_client.Pass(), this, &host_); 83 mojo::CreateViewTreeHost(host_factory, host_client.Pass(), this, &host_);
83 } 84 }
84 85
(...skipping 15 matching lines...) Expand all
100 Embed(request.Pass()); 101 Embed(request.Pass());
101 } 102 }
102 103
103 void BrowserWindow::Close() { 104 void BrowserWindow::Close() {
104 if (root_) 105 if (root_)
105 mojo::ScopedViewPtr::DeleteViewOrViewManager(root_); 106 mojo::ScopedViewPtr::DeleteViewOrViewManager(root_);
106 else 107 else
107 delete this; 108 delete this;
108 } 109 }
109 110
111 void BrowserWindow::ShowOmnibox() {
112 if (!omnibox_.get()) {
113 mojo::URLRequestPtr request(mojo::URLRequest::New());
114 request->url = mojo::String::From("mojo:omnibox");
115 omnibox_connection_ = app_->ConnectToApplication(request.Pass());
116 omnibox_connection_->AddService<ViewEmbedder>(this);
117 omnibox_connection_->ConnectToService(&omnibox_);
118 omnibox_connection_->SetRemoteServiceProviderConnectionErrorHandler(
119 [this]() {
120 // This will cause the connection to be re-established the next time
121 // we come through this codepath.
122 omnibox_.reset();
123 });
124 }
125 omnibox_->ShowForURL(mojo::String::From(current_url_.spec()));
126 }
127
128 void BrowserWindow::GoBack() {
129 web_view_.web_view()->GoBack();
130 }
131
132 void BrowserWindow::GoForward() {
133 web_view_.web_view()->GoForward();
134 }
135
110 BrowserWindow::~BrowserWindow() { 136 BrowserWindow::~BrowserWindow() {
111 DCHECK(!root_); 137 DCHECK(!root_);
112 manager_->BrowserWindowClosed(this); 138 manager_->BrowserWindowClosed(this);
113 } 139 }
114 140
115 float BrowserWindow::DIPSToPixels(float value) const { 141 float BrowserWindow::DIPSToPixels(float value) const {
116 return value * root_->viewport_metrics().device_pixel_ratio; 142 return value * root_->viewport_metrics().device_pixel_ratio;
117 } 143 }
118 144
119 //////////////////////////////////////////////////////////////////////////////// 145 ////////////////////////////////////////////////////////////////////////////////
(...skipping 19 matching lines...) Expand all
139 content_->SetVisible(true); 165 content_->SetVisible(true);
140 166
141 web_view_.Init(app_, content_); 167 web_view_.Init(app_, content_);
142 168
143 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::CLOSE), 169 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::CLOSE),
144 mojo::KEYBOARD_CODE_W, mojo::EVENT_FLAGS_CONTROL_DOWN); 170 mojo::KEYBOARD_CODE_W, mojo::EVENT_FLAGS_CONTROL_DOWN);
145 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::FOCUS_OMNIBOX), 171 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::FOCUS_OMNIBOX),
146 mojo::KEYBOARD_CODE_L, mojo::EVENT_FLAGS_CONTROL_DOWN); 172 mojo::KEYBOARD_CODE_L, mojo::EVENT_FLAGS_CONTROL_DOWN);
147 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::NEW_WINDOW), 173 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::NEW_WINDOW),
148 mojo::KEYBOARD_CODE_N, mojo::EVENT_FLAGS_CONTROL_DOWN); 174 mojo::KEYBOARD_CODE_N, mojo::EVENT_FLAGS_CONTROL_DOWN);
175 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::GO_BACK),
176 mojo::KEYBOARD_CODE_LEFT, mojo::EVENT_FLAGS_ALT_DOWN);
177 host_->AddAccelerator(static_cast<uint32_t>(BrowserCommand::GO_FORWARD),
178 mojo::KEYBOARD_CODE_RIGHT, mojo::EVENT_FLAGS_ALT_DOWN);
149 179
150 // Now that we're ready, load the default url. 180 // Now that we're ready, load the default url.
151 LoadURL(default_url_); 181 LoadURL(default_url_);
152 182
153 // Record the time spent opening initial tabs, used for performance testing. 183 // Record the time spent opening initial tabs, used for performance testing.
154 const base::TimeDelta open_tabs_delta = base::Time::Now() - display_time; 184 const base::TimeDelta open_tabs_delta = base::Time::Now() - display_time;
155 185
156 // Record the browser startup time metrics, used for performance testing. 186 // Record the browser startup time metrics, used for performance testing.
157 static bool recorded_browser_startup_metrics = false; 187 static bool recorded_browser_startup_metrics = false;
158 if (!recorded_browser_startup_metrics && 188 if (!recorded_browser_startup_metrics &&
(...skipping 23 matching lines...) Expand all
182 switch (static_cast<BrowserCommand>(id)) { 212 switch (static_cast<BrowserCommand>(id)) {
183 case BrowserCommand::CLOSE: 213 case BrowserCommand::CLOSE:
184 Close(); 214 Close();
185 break; 215 break;
186 case BrowserCommand::NEW_WINDOW: 216 case BrowserCommand::NEW_WINDOW:
187 manager_->CreateBrowser(GURL()); 217 manager_->CreateBrowser(GURL());
188 break; 218 break;
189 case BrowserCommand::FOCUS_OMNIBOX: 219 case BrowserCommand::FOCUS_OMNIBOX:
190 ShowOmnibox(); 220 ShowOmnibox();
191 break; 221 break;
222 case BrowserCommand::GO_BACK:
223 GoBack();
224 break;
225 case BrowserCommand::GO_FORWARD:
226 GoForward();
227 break;
192 default: 228 default:
193 NOTREACHED(); 229 NOTREACHED();
194 break; 230 break;
195 } 231 }
196 } 232 }
197 233
198 //////////////////////////////////////////////////////////////////////////////// 234 ////////////////////////////////////////////////////////////////////////////////
199 // BrowserWindow, web_view::mojom::WebViewClient implementation: 235 // BrowserWindow, web_view::mojom::WebViewClient implementation:
200 236
201 void BrowserWindow::TopLevelNavigate(mojo::URLRequestPtr request) { 237 void BrowserWindow::TopLevelNavigate(mojo::URLRequestPtr request) {
202 Embed(request.Pass()); 238 Embed(request.Pass());
203 } 239 }
204 240
205 void BrowserWindow::LoadingStateChanged(bool is_loading) { 241 void BrowserWindow::LoadingStateChanged(bool is_loading) {
206 progress_bar_->SetIsLoading(is_loading); 242 progress_bar_->SetIsLoading(is_loading);
207 } 243 }
208 244
209 void BrowserWindow::ProgressChanged(double progress) { 245 void BrowserWindow::ProgressChanged(double progress) {
210 progress_bar_->SetProgress(progress); 246 progress_bar_->SetProgress(progress);
211 } 247 }
212 248
249 void BrowserWindow::BackForwardChanged(
250 web_view::mojom::ButtonState back_button,
251 web_view::mojom::ButtonState forward_button) {
252 toolbar_view_->SetBackForwardEnabled(
253 back_button == web_view::mojom::ButtonState::BUTTON_STATE_ENABLED,
254 forward_button == web_view::mojom::ButtonState::BUTTON_STATE_ENABLED);
255 }
256
213 void BrowserWindow::TitleChanged(const mojo::String& title) { 257 void BrowserWindow::TitleChanged(const mojo::String& title) {
214 base::string16 formatted = 258 base::string16 formatted =
215 title.is_null() ? base::ASCIIToUTF16("Untitled") 259 title.is_null() ? base::ASCIIToUTF16("Untitled")
216 : title.To<base::string16>() + 260 : title.To<base::string16>() +
217 base::ASCIIToUTF16(" - Mandoline"); 261 base::ASCIIToUTF16(" - Mandoline");
218 host_->SetTitle(mojo::String::From(formatted)); 262 host_->SetTitle(mojo::String::From(formatted));
219 } 263 }
220 264
221 //////////////////////////////////////////////////////////////////////////////// 265 ////////////////////////////////////////////////////////////////////////////////
222 // BrowserWindow, ViewEmbedder implementation: 266 // BrowserWindow, ViewEmbedder implementation:
223 267
224 void BrowserWindow::Embed(mojo::URLRequestPtr request) { 268 void BrowserWindow::Embed(mojo::URLRequestPtr request) {
225 const std::string string_url = request->url.To<std::string>(); 269 const std::string string_url = request->url.To<std::string>();
226 if (string_url == "mojo:omnibox") { 270 if (string_url == "mojo:omnibox") {
227 EmbedOmnibox(); 271 EmbedOmnibox();
228 return; 272 return;
229 } 273 }
230 274
231 GURL gurl(string_url); 275 GURL gurl(string_url);
232 bool changed = current_url_ != gurl; 276 bool changed = current_url_ != gurl;
233 current_url_ = gurl; 277 current_url_ = gurl;
234 if (changed) 278 if (changed)
235 omnibox_launcher_->SetText(base::UTF8ToUTF16(current_url_.spec())); 279 toolbar_view_->SetOmniboxText(base::UTF8ToUTF16(current_url_.spec()));
236 280
237 web_view_.web_view()->LoadRequest(request.Pass()); 281 web_view_.web_view()->LoadRequest(request.Pass());
238 } 282 }
239 283
240 //////////////////////////////////////////////////////////////////////////////// 284 ////////////////////////////////////////////////////////////////////////////////
241 // BrowserWindow, mojo::InterfaceFactory<ViewEmbedder> implementation: 285 // BrowserWindow, mojo::InterfaceFactory<ViewEmbedder> implementation:
242 286
243 void BrowserWindow::Create(mojo::ApplicationConnection* connection, 287 void BrowserWindow::Create(mojo::ApplicationConnection* connection,
244 mojo::InterfaceRequest<ViewEmbedder> request) { 288 mojo::InterfaceRequest<ViewEmbedder> request) {
245 view_embedder_bindings_.AddBinding(this, request.Pass()); 289 view_embedder_bindings_.AddBinding(this, request.Pass());
246 } 290 }
247 291
248 //////////////////////////////////////////////////////////////////////////////// 292 ////////////////////////////////////////////////////////////////////////////////
249 // BrowserWindow, views::LayoutManager implementation: 293 // BrowserWindow, views::LayoutManager implementation:
250 294
251 gfx::Size BrowserWindow::GetPreferredSize(const views::View* view) const { 295 gfx::Size BrowserWindow::GetPreferredSize(const views::View* view) const {
252 return gfx::Size(); 296 return gfx::Size();
253 } 297 }
254 298
255 void BrowserWindow::Layout(views::View* host) { 299 void BrowserWindow::Layout(views::View* host) {
256 // TODO(fsamuel): All bounds should be in physical pixels. 300 // TODO(fsamuel): All bounds should be in physical pixels.
257 gfx::Rect bounds_in_physical_pixels(host->bounds()); 301 gfx::Rect bounds_in_physical_pixels(host->bounds());
258 float inverse_device_pixel_ratio = 302 float inverse_device_pixel_ratio =
259 1.0f / root_->viewport_metrics().device_pixel_ratio; 303 1.0f / root_->viewport_metrics().device_pixel_ratio;
260 304
261 gfx::Rect omnibox_launcher_bounds = gfx::ScaleToEnclosingRect( 305 gfx::Rect toolbar_bounds = gfx::ScaleToEnclosingRect(
262 bounds_in_physical_pixels, inverse_device_pixel_ratio); 306 bounds_in_physical_pixels, inverse_device_pixel_ratio);
263 omnibox_launcher_bounds.Inset(10, 10, 10, 307 toolbar_bounds.Inset(10, 10, 10, toolbar_bounds.height() - 40);
264 omnibox_launcher_bounds.height() - 40); 308 toolbar_view_->SetBoundsRect(toolbar_bounds);
265 omnibox_launcher_->SetBoundsRect(omnibox_launcher_bounds);
266 309
267 gfx::Rect progress_bar_bounds(omnibox_launcher_bounds.x(), 310 gfx::Rect progress_bar_bounds(toolbar_bounds.x(), toolbar_bounds.bottom() + 2,
268 omnibox_launcher_bounds.bottom() + 2, 311 toolbar_bounds.width(), 5);
269 omnibox_launcher_bounds.width(),
270 5);
271 progress_bar_->SetBoundsRect(progress_bar_bounds);
272 312
273 // The content view bounds are in physical pixels. 313 // The content view bounds are in physical pixels.
274 mojo::Rect content_bounds_mojo; 314 mojo::Rect content_bounds_mojo;
275 content_bounds_mojo.x = DIPSToPixels(progress_bar_bounds.x()); 315 content_bounds_mojo.x = DIPSToPixels(progress_bar_bounds.x());
276 content_bounds_mojo.y = DIPSToPixels(progress_bar_bounds.bottom()+ 10); 316 content_bounds_mojo.y = DIPSToPixels(progress_bar_bounds.bottom()+ 10);
277 content_bounds_mojo.width = DIPSToPixels(progress_bar_bounds.width()); 317 content_bounds_mojo.width = DIPSToPixels(progress_bar_bounds.width());
278 content_bounds_mojo.height = 318 content_bounds_mojo.height =
279 host->bounds().height() - content_bounds_mojo.y - DIPSToPixels(10); 319 host->bounds().height() - content_bounds_mojo.y - DIPSToPixels(10);
280 content_->SetBounds(content_bounds_mojo); 320 content_->SetBounds(content_bounds_mojo);
281 321
282 // The omnibox view bounds are in physical pixels. 322 // The omnibox view bounds are in physical pixels.
283 omnibox_view_->SetBounds( 323 omnibox_view_->SetBounds(
284 mojo::TypeConverter<mojo::Rect, gfx::Rect>::Convert( 324 mojo::TypeConverter<mojo::Rect, gfx::Rect>::Convert(
285 bounds_in_physical_pixels)); 325 bounds_in_physical_pixels));
286
287 } 326 }
288 327
289 //////////////////////////////////////////////////////////////////////////////// 328 ////////////////////////////////////////////////////////////////////////////////
290 // BrowserWindow, views::ButtonListener implementation:
291
292 void BrowserWindow::ButtonPressed(views::Button* sender,
293 const ui::Event& event) {
294 DCHECK_EQ(sender, omnibox_launcher_);
295 ShowOmnibox();
296 }
297
298 ////////////////////////////////////////////////////////////////////////////////
299 // BrowserWindow, private: 329 // BrowserWindow, private:
300 330
301 void BrowserWindow::Init(mojo::View* root) { 331 void BrowserWindow::Init(mojo::View* root) {
302 DCHECK_GT(root->viewport_metrics().device_pixel_ratio, 0); 332 DCHECK_GT(root->viewport_metrics().device_pixel_ratio, 0);
303 if (!aura_init_) 333 if (!aura_init_)
304 aura_init_.reset(new AuraInit(root, app_->shell())); 334 aura_init_.reset(new AuraInit(root, app_->shell()));
305 335
306 root_ = root; 336 root_ = root;
307 omnibox_view_ = root_->connection()->CreateView(); 337 omnibox_view_ = root_->connection()->CreateView();
308 root_->AddChild(omnibox_view_); 338 root_->AddChild(omnibox_view_);
309 339
310 views::WidgetDelegateView* widget_delegate = new views::WidgetDelegateView; 340 views::WidgetDelegateView* widget_delegate = new views::WidgetDelegateView;
311 widget_delegate->GetContentsView()->set_background( 341 widget_delegate->GetContentsView()->set_background(
312 views::Background::CreateSolidBackground(0xFFDDDDDD)); 342 views::Background::CreateSolidBackground(0xFFDDDDDD));
313 omnibox_launcher_ = 343 toolbar_view_ = new ToolbarView(this);
314 new views::LabelButton(this, base::ASCIIToUTF16("Open Omnibox"));
315 progress_bar_ = new ProgressView; 344 progress_bar_ = new ProgressView;
316 345 widget_delegate->GetContentsView()->AddChildView(toolbar_view_);
317 widget_delegate->GetContentsView()->AddChildView(omnibox_launcher_);
318 widget_delegate->GetContentsView()->AddChildView(progress_bar_); 346 widget_delegate->GetContentsView()->AddChildView(progress_bar_);
319 widget_delegate->GetContentsView()->SetLayoutManager(this); 347 widget_delegate->GetContentsView()->SetLayoutManager(this);
320 348
321 views::Widget* widget = new views::Widget; 349 views::Widget* widget = new views::Widget;
322 views::Widget::InitParams params( 350 views::Widget::InitParams params(
323 views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); 351 views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
324 params.native_widget = 352 params.native_widget =
325 new NativeWidgetViewManager(widget, app_->shell(), root_); 353 new NativeWidgetViewManager(widget, app_->shell(), root_);
326 params.delegate = widget_delegate; 354 params.delegate = widget_delegate;
327 params.bounds = root_->bounds().To<gfx::Rect>(); 355 params.bounds = root_->bounds().To<gfx::Rect>();
328 widget->Init(params); 356 widget->Init(params);
329 widget->Show(); 357 widget->Show();
330 root_->SetFocus(); 358 root_->SetFocus();
331 } 359 }
332 360
333 void BrowserWindow::ShowOmnibox() {
334 if (!omnibox_.get()) {
335 mojo::URLRequestPtr request(mojo::URLRequest::New());
336 request->url = mojo::String::From("mojo:omnibox");
337 omnibox_connection_ = app_->ConnectToApplication(request.Pass());
338 omnibox_connection_->AddService<ViewEmbedder>(this);
339 omnibox_connection_->ConnectToService(&omnibox_);
340 omnibox_connection_->SetRemoteServiceProviderConnectionErrorHandler(
341 [this]() {
342 // This will cause the connection to be re-established the next time
343 // we come through this codepath.
344 omnibox_.reset();
345 });
346 }
347 omnibox_->ShowForURL(mojo::String::From(current_url_.spec()));
348 }
349
350 void BrowserWindow::EmbedOmnibox() { 361 void BrowserWindow::EmbedOmnibox() {
351 mojo::ViewTreeClientPtr view_tree_client; 362 mojo::ViewTreeClientPtr view_tree_client;
352 omnibox_->GetViewTreeClient(GetProxy(&view_tree_client)); 363 omnibox_->GetViewTreeClient(GetProxy(&view_tree_client));
353 omnibox_view_->Embed(view_tree_client.Pass()); 364 omnibox_view_->Embed(view_tree_client.Pass());
354 365
355 // TODO(beng): This should be handled sufficiently by 366 // TODO(beng): This should be handled sufficiently by
356 // OmniboxImpl::ShowWindow() but unfortunately view manager policy 367 // OmniboxImpl::ShowWindow() but unfortunately view manager policy
357 // currently prevents the embedded app from changing window z for 368 // currently prevents the embedded app from changing window z for
358 // its own window. 369 // its own window.
359 omnibox_view_->MoveToFront(); 370 omnibox_view_->MoveToFront();
360 } 371 }
361 372
362 } // namespace mandoline 373 } // namespace mandoline
OLDNEW
« no previous file with comments | « mandoline/ui/desktop_ui/browser_window.h ('k') | mandoline/ui/desktop_ui/toolbar_view.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698