Browse Source

refactor: update `WebContentsZoomController` (#39428)

refactor: update WebContentsZoomController
Shelley Vohr 1 year ago
parent
commit
8e3dcc8b17

+ 1 - 0
filenames.gni

@@ -494,6 +494,7 @@ filenames = {
     "shell/browser/web_contents_preferences.h",
     "shell/browser/web_contents_zoom_controller.cc",
     "shell/browser/web_contents_zoom_controller.h",
+    "shell/browser/web_contents_zoom_observer.h",
     "shell/browser/web_view_guest_delegate.cc",
     "shell/browser/web_view_guest_delegate.h",
     "shell/browser/web_view_manager.cc",

+ 9 - 9
shell/browser/extensions/api/tabs/tabs_api.cc

@@ -42,19 +42,19 @@ void ZoomModeToZoomSettings(WebContentsZoomController::ZoomMode zoom_mode,
                             api::tabs::ZoomSettings* zoom_settings) {
   DCHECK(zoom_settings);
   switch (zoom_mode) {
-    case WebContentsZoomController::ZoomMode::kDefault:
+    case WebContentsZoomController::ZOOM_MODE_DEFAULT:
       zoom_settings->mode = api::tabs::ZOOM_SETTINGS_MODE_AUTOMATIC;
       zoom_settings->scope = api::tabs::ZOOM_SETTINGS_SCOPE_PER_ORIGIN;
       break;
-    case WebContentsZoomController::ZoomMode::kIsolated:
+    case WebContentsZoomController::ZOOM_MODE_ISOLATED:
       zoom_settings->mode = api::tabs::ZOOM_SETTINGS_MODE_AUTOMATIC;
       zoom_settings->scope = api::tabs::ZOOM_SETTINGS_SCOPE_PER_TAB;
       break;
-    case WebContentsZoomController::ZoomMode::kManual:
+    case WebContentsZoomController::ZOOM_MODE_MANUAL:
       zoom_settings->mode = api::tabs::ZOOM_SETTINGS_MODE_MANUAL;
       zoom_settings->scope = api::tabs::ZOOM_SETTINGS_SCOPE_PER_TAB;
       break;
-    case WebContentsZoomController::ZoomMode::kDisabled:
+    case WebContentsZoomController::ZOOM_MODE_DISABLED:
       zoom_settings->mode = api::tabs::ZOOM_SETTINGS_MODE_DISABLED;
       zoom_settings->scope = api::tabs::ZOOM_SETTINGS_SCOPE_PER_TAB;
       break;
@@ -427,24 +427,24 @@ ExtensionFunction::ResponseAction TabsSetZoomSettingsFunction::Run() {
   // Determine the correct internal zoom mode to set |web_contents| to from the
   // user-specified |zoom_settings|.
   WebContentsZoomController::ZoomMode zoom_mode =
-      WebContentsZoomController::ZoomMode::kDefault;
+      WebContentsZoomController::ZOOM_MODE_DEFAULT;
   switch (params->zoom_settings.mode) {
     case tabs::ZOOM_SETTINGS_MODE_NONE:
     case tabs::ZOOM_SETTINGS_MODE_AUTOMATIC:
       switch (params->zoom_settings.scope) {
         case tabs::ZOOM_SETTINGS_SCOPE_NONE:
         case tabs::ZOOM_SETTINGS_SCOPE_PER_ORIGIN:
-          zoom_mode = WebContentsZoomController::ZoomMode::kDefault;
+          zoom_mode = WebContentsZoomController::ZOOM_MODE_DEFAULT;
           break;
         case tabs::ZOOM_SETTINGS_SCOPE_PER_TAB:
-          zoom_mode = WebContentsZoomController::ZoomMode::kIsolated;
+          zoom_mode = WebContentsZoomController::ZOOM_MODE_ISOLATED;
       }
       break;
     case tabs::ZOOM_SETTINGS_MODE_MANUAL:
-      zoom_mode = WebContentsZoomController::ZoomMode::kManual;
+      zoom_mode = WebContentsZoomController::ZOOM_MODE_MANUAL;
       break;
     case tabs::ZOOM_SETTINGS_MODE_DISABLED:
-      zoom_mode = WebContentsZoomController::ZoomMode::kDisabled;
+      zoom_mode = WebContentsZoomController::ZOOM_MODE_DISABLED;
   }
 
   contents->GetZoomController()->SetZoomMode(zoom_mode);

+ 189 - 62
shell/browser/web_contents_zoom_controller.cc

@@ -6,6 +6,7 @@
 
 #include <string>
 
+#include "content/public/browser/browser_thread.h"
 #include "content/public/browser/navigation_details.h"
 #include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/navigation_handle.h"
@@ -16,72 +17,122 @@
 #include "content/public/browser/web_contents_user_data.h"
 #include "content/public/common/page_type.h"
 #include "net/base/url_util.h"
+#include "shell/browser/web_contents_zoom_observer.h"
 #include "third_party/blink/public/common/page/page_zoom.h"
 
+using content::BrowserThread;
+
 namespace electron {
 
+namespace {
+
+const double kPageZoomEpsilon = 0.001;
+
+}  // namespace
+
 WebContentsZoomController::WebContentsZoomController(
     content::WebContents* web_contents)
     : content::WebContentsObserver(web_contents),
       content::WebContentsUserData<WebContentsZoomController>(*web_contents) {
-  default_zoom_factor_ = kPageZoomEpsilon;
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   host_zoom_map_ = content::HostZoomMap::GetForWebContents(web_contents);
+  zoom_level_ = host_zoom_map_->GetDefaultZoomLevel();
+  default_zoom_factor_ = kPageZoomEpsilon;
+
+  zoom_subscription_ = host_zoom_map_->AddZoomLevelChangedCallback(
+      base::BindRepeating(&WebContentsZoomController::OnZoomLevelChanged,
+                          base::Unretained(this)));
+
+  UpdateState(std::string());
 }
 
-WebContentsZoomController::~WebContentsZoomController() = default;
+WebContentsZoomController::~WebContentsZoomController() {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  for (auto& observer : observers_) {
+    observer.OnZoomControllerDestroyed(this);
+  }
+}
 
-void WebContentsZoomController::AddObserver(
-    WebContentsZoomController::Observer* observer) {
+void WebContentsZoomController::AddObserver(WebContentsZoomObserver* observer) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   observers_.AddObserver(observer);
 }
 
 void WebContentsZoomController::RemoveObserver(
-    WebContentsZoomController::Observer* observer) {
+    WebContentsZoomObserver* observer) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   observers_.RemoveObserver(observer);
 }
 
 void WebContentsZoomController::SetEmbedderZoomController(
     WebContentsZoomController* controller) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   embedder_zoom_controller_ = controller;
 }
 
-void WebContentsZoomController::SetZoomLevel(double level) {
-  if (!web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive() ||
-      blink::PageZoomValuesEqual(GetZoomLevel(), level) ||
-      zoom_mode_ == ZoomMode::kDisabled)
-    return;
-
-  content::GlobalRenderFrameHostId rfh_id =
-      web_contents()->GetPrimaryMainFrame()->GetGlobalId();
-
-  if (zoom_mode_ == ZoomMode::kManual) {
+bool WebContentsZoomController::SetZoomLevel(double level) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  content::NavigationEntry* entry =
+      web_contents()->GetController().GetLastCommittedEntry();
+  // Cannot zoom in disabled mode. Also, don't allow changing zoom level on
+  // a crashed tab, an error page or an interstitial page.
+  if (zoom_mode_ == ZOOM_MODE_DISABLED ||
+      !web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive())
+    return false;
+
+  // Do not actually rescale the page in manual mode.
+  if (zoom_mode_ == ZOOM_MODE_MANUAL) {
+    // If the zoom level hasn't changed, early out to avoid sending an event.
+    if (blink::PageZoomValuesEqual(zoom_level_, level))
+      return true;
+
+    double old_zoom_level = zoom_level_;
     zoom_level_ = level;
 
-    for (Observer& observer : observers_)
-      observer.OnZoomLevelChanged(web_contents(), level, true);
+    ZoomChangedEventData zoom_change_data(web_contents(), old_zoom_level,
+                                          zoom_level_, false /* temporary */,
+                                          zoom_mode_);
+    for (auto& observer : observers_)
+      observer.OnZoomChanged(zoom_change_data);
 
-    return;
+    return true;
   }
 
   content::HostZoomMap* zoom_map =
       content::HostZoomMap::GetForWebContents(web_contents());
-  if (zoom_mode_ == ZoomMode::kIsolated ||
+  DCHECK(zoom_map);
+  DCHECK(!event_data_);
+  event_data_ = std::make_unique<ZoomChangedEventData>(
+      web_contents(), GetZoomLevel(), level, false /* temporary */, zoom_mode_);
+
+  content::GlobalRenderFrameHostId rfh_id =
+      web_contents()->GetPrimaryMainFrame()->GetGlobalId();
+  if (zoom_mode_ == ZOOM_MODE_ISOLATED ||
       zoom_map->UsesTemporaryZoomLevel(rfh_id)) {
     zoom_map->SetTemporaryZoomLevel(rfh_id, level);
-    // Notify observers of zoom level changes.
-    for (Observer& observer : observers_)
-      observer.OnZoomLevelChanged(web_contents(), level, true);
+    ZoomChangedEventData zoom_change_data(web_contents(), zoom_level_, level,
+                                          true /* temporary */, zoom_mode_);
+    for (auto& observer : observers_)
+      observer.OnZoomChanged(zoom_change_data);
   } else {
-    content::HostZoomMap::SetZoomLevel(web_contents(), level);
-
-    // Notify observers of zoom level changes.
-    for (Observer& observer : observers_)
-      observer.OnZoomLevelChanged(web_contents(), level, false);
+    if (!entry) {
+      // If we exit without triggering an update, we should clear event_data_,
+      // else we may later trigger a DCHECK(event_data_).
+      event_data_.reset();
+      return false;
+    }
+    std::string host =
+        net::GetHostOrSpecFromURL(content::HostZoomMap::GetURLFromEntry(entry));
+    zoom_map->SetZoomLevelForHost(host, level);
   }
+
+  DCHECK(!event_data_);
+  return true;
 }
 
-double WebContentsZoomController::GetZoomLevel() {
-  return zoom_mode_ == ZoomMode::kManual
+double WebContentsZoomController::GetZoomLevel() const {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  return zoom_mode_ == ZOOM_MODE_MANUAL
              ? zoom_level_
              : content::HostZoomMap::GetZoomLevel(web_contents());
 }
@@ -95,32 +146,44 @@ double WebContentsZoomController::GetDefaultZoomFactor() {
 }
 
 void WebContentsZoomController::SetTemporaryZoomLevel(double level) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   content::GlobalRenderFrameHostId old_rfh_id_ =
       web_contents()->GetPrimaryMainFrame()->GetGlobalId();
   host_zoom_map_->SetTemporaryZoomLevel(old_rfh_id_, level);
+
   // Notify observers of zoom level changes.
-  for (Observer& observer : observers_)
-    observer.OnZoomLevelChanged(web_contents(), level, true);
+  ZoomChangedEventData zoom_change_data(web_contents(), zoom_level_, level,
+                                        true /* temporary */, zoom_mode_);
+  for (WebContentsZoomObserver& observer : observers_)
+    observer.OnZoomChanged(zoom_change_data);
 }
 
 bool WebContentsZoomController::UsesTemporaryZoomLevel() {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   content::GlobalRenderFrameHostId rfh_id =
       web_contents()->GetPrimaryMainFrame()->GetGlobalId();
   return host_zoom_map_->UsesTemporaryZoomLevel(rfh_id);
 }
 
 void WebContentsZoomController::SetZoomMode(ZoomMode new_mode) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   if (new_mode == zoom_mode_)
     return;
 
   content::HostZoomMap* zoom_map =
       content::HostZoomMap::GetForWebContents(web_contents());
+  DCHECK(zoom_map);
   content::GlobalRenderFrameHostId rfh_id =
       web_contents()->GetPrimaryMainFrame()->GetGlobalId();
   double original_zoom_level = GetZoomLevel();
 
+  DCHECK(!event_data_);
+  event_data_ = std::make_unique<ZoomChangedEventData>(
+      web_contents(), original_zoom_level, original_zoom_level,
+      false /* temporary */, new_mode);
+
   switch (new_mode) {
-    case ZoomMode::kDefault: {
+    case ZOOM_MODE_DEFAULT: {
       content::NavigationEntry* entry =
           web_contents()->GetController().GetLastCommittedEntry();
 
@@ -135,6 +198,7 @@ void WebContentsZoomController::SetZoomMode(ZoomMode new_mode) {
           // the correct zoom level.
           double origin_zoom_level =
               zoom_map->GetZoomLevelForHostAndScheme(url.scheme(), host);
+          event_data_->new_zoom_level = origin_zoom_level;
           zoom_map->SetTemporaryZoomLevel(rfh_id, origin_zoom_level);
         } else {
           // The host will need a level prior to removing the temporary level.
@@ -147,101 +211,128 @@ void WebContentsZoomController::SetZoomMode(ZoomMode new_mode) {
       zoom_map->ClearTemporaryZoomLevel(rfh_id);
       break;
     }
-    case ZoomMode::kIsolated: {
-      // Unless the zoom mode was |ZoomMode::kDisabled| before this call, the
+    case ZOOM_MODE_ISOLATED: {
+      // Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
       // page needs an initial isolated zoom back to the same level it was at
       // in the other mode.
-      if (zoom_mode_ != ZoomMode::kDisabled) {
+      if (zoom_mode_ != ZOOM_MODE_DISABLED) {
         zoom_map->SetTemporaryZoomLevel(rfh_id, original_zoom_level);
       } else {
         // When we don't call any HostZoomMap set functions, we send the event
         // manually.
-        for (Observer& observer : observers_)
-          observer.OnZoomLevelChanged(web_contents(), original_zoom_level,
-                                      false);
+        for (auto& observer : observers_)
+          observer.OnZoomChanged(*event_data_);
+        event_data_.reset();
       }
       break;
     }
-    case ZoomMode::kManual: {
-      // Unless the zoom mode was |ZoomMode::kDisabled| before this call, the
+    case ZOOM_MODE_MANUAL: {
+      // Unless the zoom mode was |ZOOM_MODE_DISABLED| before this call, the
       // page needs to be resized to the default zoom. While in manual mode,
       // the zoom level is handled independently.
-      if (zoom_mode_ != ZoomMode::kDisabled) {
+      if (zoom_mode_ != ZOOM_MODE_DISABLED) {
         zoom_map->SetTemporaryZoomLevel(rfh_id, GetDefaultZoomLevel());
         zoom_level_ = original_zoom_level;
       } else {
         // When we don't call any HostZoomMap set functions, we send the event
         // manually.
-        for (Observer& observer : observers_)
-          observer.OnZoomLevelChanged(web_contents(), original_zoom_level,
-                                      false);
+        for (auto& observer : observers_)
+          observer.OnZoomChanged(*event_data_);
+        event_data_.reset();
       }
       break;
     }
-    case ZoomMode::kDisabled: {
+    case ZOOM_MODE_DISABLED: {
       // The page needs to be zoomed back to default before disabling the zoom
-      zoom_map->SetTemporaryZoomLevel(rfh_id, GetDefaultZoomLevel());
+      double new_zoom_level = GetDefaultZoomLevel();
+      event_data_->new_zoom_level = new_zoom_level;
+      zoom_map->SetTemporaryZoomLevel(rfh_id, new_zoom_level);
       break;
     }
   }
+  // Any event data we've stored should have been consumed by this point.
+  DCHECK(!event_data_);
 
   zoom_mode_ = new_mode;
 }
 
 void WebContentsZoomController::ResetZoomModeOnNavigationIfNeeded(
     const GURL& url) {
-  if (zoom_mode_ != ZoomMode::kIsolated && zoom_mode_ != ZoomMode::kManual)
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  if (zoom_mode_ != ZOOM_MODE_ISOLATED && zoom_mode_ != ZOOM_MODE_MANUAL)
     return;
 
-  content::GlobalRenderFrameHostId rfh_id =
-      web_contents()->GetPrimaryMainFrame()->GetGlobalId();
   content::HostZoomMap* zoom_map =
       content::HostZoomMap::GetForWebContents(web_contents());
   zoom_level_ = zoom_map->GetDefaultZoomLevel();
+  double old_zoom_level = zoom_map->GetZoomLevel(web_contents());
   double new_zoom_level = zoom_map->GetZoomLevelForHostAndScheme(
       url.scheme(), net::GetHostOrSpecFromURL(url));
-  for (Observer& observer : observers_)
-    observer.OnZoomLevelChanged(web_contents(), new_zoom_level, false);
-  zoom_map->ClearTemporaryZoomLevel(rfh_id);
-  zoom_mode_ = ZoomMode::kDefault;
+  event_data_ = std::make_unique<ZoomChangedEventData>(
+      web_contents(), old_zoom_level, new_zoom_level, false, ZOOM_MODE_DEFAULT);
+  // The call to ClearTemporaryZoomLevel() doesn't generate any events from
+  // HostZoomMap, but the call to UpdateState() at the end of
+  // DidFinishNavigation will notify our observers.
+  // Note: it's possible the render_process/frame ids have disappeared (e.g.
+  // if we navigated to a new origin), but this won't cause a problem in the
+  // call below.
+  zoom_map->ClearTemporaryZoomLevel(
+      web_contents()->GetPrimaryMainFrame()->GetGlobalId());
+  zoom_mode_ = ZOOM_MODE_DEFAULT;
 }
 
 void WebContentsZoomController::DidFinishNavigation(
     content::NavigationHandle* navigation_handle) {
-  if (!navigation_handle->IsInMainFrame() || !navigation_handle->HasCommitted())
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  if (!navigation_handle->IsInPrimaryMainFrame() ||
+      !navigation_handle->HasCommitted()) {
     return;
+  }
 
-  if (navigation_handle->IsErrorPage()) {
+  if (navigation_handle->IsErrorPage())
     content::HostZoomMap::SendErrorPageZoomLevelRefresh(web_contents());
-    return;
+
+  if (!navigation_handle->IsSameDocument()) {
+    ResetZoomModeOnNavigationIfNeeded(navigation_handle->GetURL());
+    SetZoomFactorOnNavigationIfNeeded(navigation_handle->GetURL());
   }
 
-  ResetZoomModeOnNavigationIfNeeded(navigation_handle->GetURL());
-  SetZoomFactorOnNavigationIfNeeded(navigation_handle->GetURL());
+  // If the main frame's content has changed, the new page may have a different
+  // zoom level from the old one.
+  UpdateState(std::string());
+  DCHECK(!event_data_);
 }
 
 void WebContentsZoomController::WebContentsDestroyed() {
-  for (Observer& observer : observers_)
-    observer.OnZoomControllerWebContentsDestroyed();
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  // At this point we should no longer be sending any zoom events with this
+  // WebContents.
+  for (auto& observer : observers_) {
+    observer.OnZoomControllerDestroyed(this);
+  }
 
-  observers_.Clear();
   embedder_zoom_controller_ = nullptr;
 }
 
 void WebContentsZoomController::RenderFrameHostChanged(
     content::RenderFrameHost* old_host,
     content::RenderFrameHost* new_host) {
-  // If our associated HostZoomMap changes, update our event subscription.
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  // If our associated HostZoomMap changes, update our subscription.
   content::HostZoomMap* new_host_zoom_map =
       content::HostZoomMap::GetForWebContents(web_contents());
   if (new_host_zoom_map == host_zoom_map_)
     return;
 
   host_zoom_map_ = new_host_zoom_map;
+  zoom_subscription_ = host_zoom_map_->AddZoomLevelChangedCallback(
+      base::BindRepeating(&WebContentsZoomController::OnZoomLevelChanged,
+                          base::Unretained(this)));
 }
 
 void WebContentsZoomController::SetZoomFactorOnNavigationIfNeeded(
     const GURL& url) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   if (blink::PageZoomValuesEqual(GetDefaultZoomFactor(), kPageZoomEpsilon))
     return;
 
@@ -276,6 +367,42 @@ void WebContentsZoomController::SetZoomFactorOnNavigationIfNeeded(
   SetZoomLevel(zoom_level);
 }
 
+void WebContentsZoomController::OnZoomLevelChanged(
+    const content::HostZoomMap::ZoomLevelChange& change) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  UpdateState(change.host);
+}
+
+void WebContentsZoomController::UpdateState(const std::string& host) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  // If |host| is empty, all observers should be updated.
+  if (!host.empty()) {
+    // Use the navigation entry's URL instead of the WebContents' so virtual
+    // URLs work (e.g. chrome://settings). http://crbug.com/153950
+    content::NavigationEntry* entry =
+        web_contents()->GetController().GetLastCommittedEntry();
+    if (!entry || host != net::GetHostOrSpecFromURL(
+                              content::HostZoomMap::GetURLFromEntry(entry))) {
+      return;
+    }
+  }
+
+  if (event_data_) {
+    // For state changes initiated within the ZoomController, information about
+    // the change should be sent.
+    ZoomChangedEventData zoom_change_data = *event_data_;
+    event_data_.reset();
+    for (auto& observer : observers_)
+      observer.OnZoomChanged(zoom_change_data);
+  } else {
+    double zoom_level = GetZoomLevel();
+    ZoomChangedEventData zoom_change_data(web_contents(), zoom_level,
+                                          zoom_level, false, zoom_mode_);
+    for (auto& observer : observers_)
+      observer.OnZoomChanged(zoom_change_data);
+  }
+}
+
 WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsZoomController);
 
 }  // namespace electron

+ 60 - 34
shell/browser/web_contents_zoom_controller.h

@@ -14,40 +14,49 @@
 
 namespace electron {
 
+class WebContentsZoomObserver;
+
 // Manages the zoom changes of WebContents.
 class WebContentsZoomController
     : public content::WebContentsObserver,
       public content::WebContentsUserData<WebContentsZoomController> {
  public:
-  class Observer : public base::CheckedObserver {
-   public:
-    virtual void OnZoomLevelChanged(content::WebContents* web_contents,
-                                    double level,
-                                    bool is_temporary) {}
-    virtual void OnZoomControllerWebContentsDestroyed() {}
-
-   protected:
-    ~Observer() override {}
-  };
-
   // Defines how zoom changes are handled.
-  enum class ZoomMode {
+  enum ZoomMode {
     // Results in default zoom behavior, i.e. zoom changes are handled
     // automatically and on a per-origin basis, meaning that other tabs
     // navigated to the same origin will also zoom.
-    kDefault,
+    ZOOM_MODE_DEFAULT,
     // Results in zoom changes being handled automatically, but on a per-tab
     // basis. Tabs in this zoom mode will not be affected by zoom changes in
     // other tabs, and vice versa.
-    kIsolated,
+    ZOOM_MODE_ISOLATED,
     // Overrides the automatic handling of zoom changes. The |onZoomChange|
     // event will still be dispatched, but the page will not actually be zoomed.
     // These zoom changes can be handled manually by listening for the
     // |onZoomChange| event. Zooming in this mode is also on a per-tab basis.
-    kManual,
+    ZOOM_MODE_MANUAL,
     // Disables all zooming in this tab. The tab will revert to the default
     // zoom level, and all attempted zoom changes will be ignored.
-    kDisabled,
+    ZOOM_MODE_DISABLED,
+  };
+
+  struct ZoomChangedEventData {
+    ZoomChangedEventData(content::WebContents* web_contents,
+                         double old_zoom_level,
+                         double new_zoom_level,
+                         bool temporary,
+                         WebContentsZoomController::ZoomMode zoom_mode)
+        : web_contents(web_contents),
+          old_zoom_level(old_zoom_level),
+          new_zoom_level(new_zoom_level),
+          temporary(temporary),
+          zoom_mode(zoom_mode) {}
+    raw_ptr<content::WebContents> web_contents;
+    double old_zoom_level;
+    double new_zoom_level;
+    bool temporary;
+    WebContentsZoomController::ZoomMode zoom_mode;
   };
 
   explicit WebContentsZoomController(content::WebContents* web_contents);
@@ -58,24 +67,29 @@ class WebContentsZoomController
   WebContentsZoomController& operator=(const WebContentsZoomController&) =
       delete;
 
-  void AddObserver(Observer* observer);
-  void RemoveObserver(Observer* observer);
+  void AddObserver(WebContentsZoomObserver* observer);
+  void RemoveObserver(WebContentsZoomObserver* observer);
 
   void SetEmbedderZoomController(WebContentsZoomController* controller);
 
-  // Methods for managing zoom levels.
-  void SetZoomLevel(double level);
-  double GetZoomLevel();
+  // Gets the current zoom level by querying HostZoomMap (if not in manual zoom
+  // mode) or from the ZoomController local value otherwise.
+  double GetZoomLevel() const;
+
+  // Sets the zoom level through HostZoomMap.
+  // Returns true on success.
+  bool SetZoomLevel(double zoom_level);
+
   void SetDefaultZoomFactor(double factor);
   double GetDefaultZoomFactor();
+
+  // Sets the temporary zoom level through HostZoomMap.
   void SetTemporaryZoomLevel(double level);
   bool UsesTemporaryZoomLevel();
 
   // Sets the zoom mode, which defines zoom behavior (see enum ZoomMode).
   void SetZoomMode(ZoomMode zoom_mode);
 
-  void ResetZoomModeOnNavigationIfNeeded(const GURL& url);
-
   ZoomMode zoom_mode() const { return zoom_mode_; }
 
   // Convenience method to get default zoom level. Implemented here for
@@ -86,8 +100,9 @@ class WebContentsZoomController
   }
 
  protected:
-  // content::WebContentsObserver:
-  void DidFinishNavigation(content::NavigationHandle* handle) override;
+  // content::WebContentsObserver overrides:
+  void DidFinishNavigation(
+      content::NavigationHandle* navigation_handle) override;
   void WebContentsDestroyed() override;
   void RenderFrameHostChanged(content::RenderFrameHost* old_host,
                               content::RenderFrameHost* new_host) override;
@@ -95,29 +110,40 @@ class WebContentsZoomController
  private:
   friend class content::WebContentsUserData<WebContentsZoomController>;
 
-  // Called after a navigation has committed to set default zoom factor.
+  void ResetZoomModeOnNavigationIfNeeded(const GURL& url);
   void SetZoomFactorOnNavigationIfNeeded(const GURL& url);
+  void OnZoomLevelChanged(const content::HostZoomMap::ZoomLevelChange& change);
 
-  // The current zoom mode.
-  ZoomMode zoom_mode_ = ZoomMode::kDefault;
+  // Updates the zoom icon and zoom percentage based on current values and
+  // notifies the observer if changes have occurred. |host| may be empty,
+  // meaning the change should apply to ~all sites. If it is not empty, the
+  // change only affects sites with the given host.
+  void UpdateState(const std::string& host);
 
-  // Current zoom level.
-  double zoom_level_ = 1.0;
+  // The current zoom mode.
+  ZoomMode zoom_mode_ = ZOOM_MODE_DEFAULT;
 
-  // kZoomFactor.
-  double default_zoom_factor_ = 0;
+  // The current zoom level.
+  double zoom_level_;
 
-  const double kPageZoomEpsilon = 0.001;
+  // The current default zoom factor.
+  double default_zoom_factor_;
 
   int old_process_id_ = -1;
   int old_view_id_ = -1;
 
+  std::unique_ptr<ZoomChangedEventData> event_data_;
+
   raw_ptr<WebContentsZoomController> embedder_zoom_controller_ = nullptr;
 
-  base::ObserverList<Observer> observers_;
+  // Observer receiving notifications on state changes.
+  base::ObserverList<WebContentsZoomObserver> observers_;
 
+  // Keep track of the HostZoomMap we're currently subscribed to.
   raw_ptr<content::HostZoomMap> host_zoom_map_;
 
+  base::CallbackListSubscription zoom_subscription_;
+
   WEB_CONTENTS_USER_DATA_KEY_DECL();
 };
 

+ 29 - 0
shell/browser/web_contents_zoom_observer.h

@@ -0,0 +1,29 @@
+// Copyright (c) 2023 Microsoft, GmbH
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_BROWSER_WEB_CONTENTS_ZOOM_OBSERVER_H_
+#define ELECTRON_SHELL_BROWSER_WEB_CONTENTS_ZOOM_OBSERVER_H_
+
+#include "shell/browser/web_contents_zoom_controller.h"
+
+namespace electron {
+
+// Interface for objects that wish to be notified of changes in
+// WebContentsZoomController.
+class WebContentsZoomObserver : public base::CheckedObserver {
+ public:
+  // Fired when the ZoomController is destructed. Observers should deregister
+  // themselves from the ZoomObserver in this event handler. Note that
+  // ZoomController::FromWebContents() returns nullptr at this point already.
+  virtual void OnZoomControllerDestroyed(
+      WebContentsZoomController* zoom_controller) = 0;
+
+  // Notification that the zoom percentage has changed.
+  virtual void OnZoomChanged(
+      const WebContentsZoomController::ZoomChangedEventData& data) {}
+};
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_BROWSER_WEB_CONTENTS_ZOOM_OBSERVER_H_

+ 10 - 10
shell/browser/web_view_guest_delegate.cc

@@ -73,23 +73,23 @@ content::WebContents* WebViewGuestDelegate::GetOwnerWebContents() {
   return embedder_web_contents_;
 }
 
-void WebViewGuestDelegate::OnZoomLevelChanged(
-    content::WebContents* web_contents,
-    double level,
-    bool is_temporary) {
-  if (web_contents == GetOwnerWebContents()) {
-    if (is_temporary) {
-      api_web_contents_->GetZoomController()->SetTemporaryZoomLevel(level);
+void WebViewGuestDelegate::OnZoomChanged(
+    const WebContentsZoomController::ZoomChangedEventData& data) {
+  if (data.web_contents == GetOwnerWebContents()) {
+    if (data.temporary) {
+      api_web_contents_->GetZoomController()->SetTemporaryZoomLevel(
+          data.new_zoom_level);
     } else {
-      api_web_contents_->GetZoomController()->SetZoomLevel(level);
+      api_web_contents_->GetZoomController()->SetZoomLevel(data.new_zoom_level);
     }
     // Change the default zoom factor to match the embedders' new zoom level.
-    double zoom_factor = blink::PageZoomLevelToZoomFactor(level);
+    double zoom_factor = blink::PageZoomLevelToZoomFactor(data.new_zoom_level);
     api_web_contents_->GetZoomController()->SetDefaultZoomFactor(zoom_factor);
   }
 }
 
-void WebViewGuestDelegate::OnZoomControllerWebContentsDestroyed() {
+void WebViewGuestDelegate::OnZoomControllerDestroyed(
+    WebContentsZoomController* zoom_controller) {
   ResetZoomController();
 }
 

+ 7 - 6
shell/browser/web_view_guest_delegate.h

@@ -10,6 +10,7 @@
 #include "base/memory/raw_ptr.h"
 #include "content/public/browser/browser_plugin_guest_delegate.h"
 #include "shell/browser/web_contents_zoom_controller.h"
+#include "shell/browser/web_contents_zoom_observer.h"
 
 namespace electron {
 
@@ -18,7 +19,7 @@ class WebContents;
 }
 
 class WebViewGuestDelegate : public content::BrowserPluginGuestDelegate,
-                             public WebContentsZoomController::Observer {
+                             public WebContentsZoomObserver {
  public:
   WebViewGuestDelegate(content::WebContents* embedder,
                        api::WebContents* api_web_contents);
@@ -41,11 +42,11 @@ class WebViewGuestDelegate : public content::BrowserPluginGuestDelegate,
   base::WeakPtr<content::BrowserPluginGuestDelegate> GetGuestDelegateWeakPtr()
       final;
 
-  // WebContentsZoomController::Observer:
-  void OnZoomLevelChanged(content::WebContents* web_contents,
-                          double level,
-                          bool is_temporary) override;
-  void OnZoomControllerWebContentsDestroyed() override;
+  // WebContentsZoomObserver:
+  void OnZoomControllerDestroyed(
+      WebContentsZoomController* zoom_controller) override;
+  void OnZoomChanged(
+      const WebContentsZoomController::ZoomChangedEventData& data) override;
 
  private:
   void ResetZoomController();