From 3b147205baaf77fe7c6163a63e4a0d42cc3ac2b0 Mon Sep 17 00:00:00 2001
From: Calixte Denizet <calixte.denizet@gmail.com>
Date: Fri, 7 Apr 2023 10:28:53 +0200
Subject: [PATCH] [GeckoView] Add a basic toolbar with a download button for GV
 (bug 1823164)

---
 gulpfile.js               |  5 ++-
 web/app.js                | 15 +++++++-
 web/toolbar-geckoview.js  | 79 +++++++++++++++++++++++++++++++++++++++
 web/viewer-geckoview.css  | 76 +++++++++++++++++++++++++++++++++++++
 web/viewer-geckoview.html |  8 +++-
 web/viewer-geckoview.js   |  8 +++-
 6 files changed, 186 insertions(+), 5 deletions(-)
 create mode 100644 web/toolbar-geckoview.js

diff --git a/gulpfile.js b/gulpfile.js
index 243cceb7f..b8d384940 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -264,8 +264,11 @@ function createWebpackConfig(
     viewerAlias["web-print_service"] = "web/pdf_print_service.js";
   } else if (bundleDefines.MOZCENTRAL) {
     if (bundleDefines.GECKOVIEW) {
+      const gvAlias = {
+        "web-toolbar": "web/toolbar-geckoview.js",
+      };
       for (const key in viewerAlias) {
-        viewerAlias[key] = "web/stubs-geckoview.js";
+        viewerAlias[key] = gvAlias[key] || "web/stubs-geckoview.js";
       }
     } else {
       viewerAlias["web-print_service"] = "web/firefox_print_service.js";
diff --git a/web/app.js b/web/app.js
index ea1cca5b5..085d13b0f 100644
--- a/web/app.js
+++ b/web/app.js
@@ -2234,7 +2234,7 @@ function webViewerInitialized() {
   }
 
   if (!PDFViewerApplication.supportsPrinting) {
-    appConfig.toolbar?.print.classList.add("hidden");
+    appConfig.toolbar?.print?.classList.add("hidden");
     appConfig.secondaryToolbar?.printButton.classList.add("hidden");
   }
 
@@ -2243,7 +2243,7 @@ function webViewerInitialized() {
   }
 
   if (PDFViewerApplication.supportsIntegratedFind) {
-    appConfig.toolbar?.viewFind.classList.add("hidden");
+    appConfig.toolbar?.viewFind?.classList.add("hidden");
   }
 
   appConfig.mainContainer.addEventListener(
@@ -2917,6 +2917,17 @@ function webViewerTouchEnd(evt) {
 }
 
 function webViewerClick(evt) {
+  if (
+    typeof PDFJSDev === "undefined"
+      ? window.isGECKOVIEW
+      : PDFJSDev.test("GECKOVIEW")
+  ) {
+    if (
+      document.activeElement === PDFViewerApplication.appConfig.mainContainer
+    ) {
+      PDFViewerApplication.toolbar?.toggle();
+    }
+  }
   if (!PDFViewerApplication.secondaryToolbar?.isOpen) {
     return;
   }
diff --git a/web/toolbar-geckoview.js b/web/toolbar-geckoview.js
new file mode 100644
index 000000000..adfa8f644
--- /dev/null
+++ b/web/toolbar-geckoview.js
@@ -0,0 +1,79 @@
+/* Copyright 2023 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @typedef {Object} ToolbarOptions
+ * @property {HTMLDivElement} mainContainer - Main container.
+ * @property {HTMLDivElement} container - Container for the toolbar.
+ * @property {HTMLButtonElement} download - Button to download the document.
+ */
+
+class Toolbar {
+  #buttons;
+
+  #eventBus;
+
+  #toolbar;
+
+  #mainContainer;
+
+  #toggleBound = this.toggle.bind(this);
+
+  /**
+   * @param {ToolbarOptions} options
+   * @param {EventBus} eventBus
+   * @param {IL10n} _l10n - Localization service.
+   */
+  constructor(options, eventBus, _l10n) {
+    this.#toolbar = options.container;
+    this.#mainContainer = options.mainContainer;
+    this.#eventBus = eventBus;
+    this.#buttons = [{ element: options.download, eventName: "download" }];
+
+    // Bind the event listeners for click and various other actions.
+    this.#bindListeners(options);
+  }
+
+  setPageNumber(pageNumber, pageLabel) {}
+
+  setPagesCount(pagesCount, hasPageLabels) {}
+
+  setPageScale(pageScaleValue, pageScale) {}
+
+  reset() {}
+
+  #bindListeners(options) {
+    // The buttons within the toolbar.
+    for (const { element, eventName, eventDetails } of this.#buttons) {
+      element.addEventListener("click", evt => {
+        if (eventName !== null) {
+          this.#eventBus.dispatch(eventName, { source: this, ...eventDetails });
+        }
+      });
+    }
+  }
+
+  updateLoadingIndicatorState(loading = false) {}
+
+  toggle() {
+    if (this.#toolbar.classList.toggle("show")) {
+      this.#mainContainer.addEventListener("scroll", this.#toggleBound);
+    } else {
+      this.#mainContainer.removeEventListener("scroll", this.#toggleBound);
+    }
+  }
+}
+
+export { Toolbar };
diff --git a/web/viewer-geckoview.css b/web/viewer-geckoview.css
index cc8d14381..6a37002fa 100644
--- a/web/viewer-geckoview.css
+++ b/web/viewer-geckoview.css
@@ -29,6 +29,8 @@
   --dialog-button-border: none;
   --dialog-button-bg-color: rgba(12, 12, 13, 0.1);
   --dialog-button-hover-bg-color: rgba(12, 12, 13, 0.3);
+
+  --toolbarButton-download-icon: url(images/toolbarButton-download.svg);
 }
 
 :root:dir(rtl) {
@@ -149,6 +151,80 @@ body {
   border-color: #0a84ff;
 }
 
+#floatingToolbar {
+  position: absolute;
+  width: 40px;
+  height: auto;
+  bottom: 5%;
+  right: 5%;
+  background-color: transparent;
+  z-index: 100000;
+}
+
+#floatingToolbar.show {
+  display: block;
+}
+
+#floatingToolbar:not(show) {
+  display: none;
+}
+
+.toolbarButton {
+  margin: 2px;
+  padding: 8px;
+  border-style: solid;
+  border-width: 1px;
+  border-color: transparent;
+  border-radius: 19px;
+  user-select: none;
+  box-sizing: border-box;
+  background-color: transparent;
+  backdrop-filter: blur(20px) contrast(100%) invert(100%);
+  width: 38px;
+  height: 38px;
+  outline: none;
+  position: relative;
+}
+
+.toolbarButton > span {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+}
+
+.toolbarButton[disabled],
+.dialogButton[disabled] {
+  opacity: 0.5;
+}
+
+.toolbarButton:hover,
+.toolbarButton:focus-visible {
+  backdrop-filter: blur(20px) contrast(200%) invert(100%);
+}
+
+.toolbarButton::before {
+  display: inline-block;
+  width: 100%;
+  height: 100%;
+  content: "";
+  background-color: transparent;
+  backdrop-filter: invert(100%);
+  mask-size: cover;
+}
+
+.toolbarButton::before {
+  opacity: var(--toolbar-icon-opacity);
+}
+
+.toolbarButton:hover::before {
+  backdrop-filter: invert(60%);
+}
+
+#download::before {
+  mask-image: var(--toolbarButton-download-icon);
+}
+
 .dialogButton {
   width: auto;
   margin: 3px 4px 2px !important;
diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html
index 03f3f64b0..a13ba8195 100644
--- a/web/viewer-geckoview.html
+++ b/web/viewer-geckoview.html
@@ -64,7 +64,7 @@ See https://github.com/adobe-type-tools/cmap-resources
           "web-pdf_thumbnail_viewer": "./stubs-geckoview.js",
           "web-print_service": "./stubs-geckoview.js",
           "web-secondary_toolbar": "./stubs-geckoview.js",
-          "web-toolbar": "./stubs-geckoview.js"
+          "web-toolbar": "./toolbar-geckoview.js"
         }
       }
     </script>
@@ -103,6 +103,12 @@ See https://github.com/adobe-type-tools/cmap-resources
 
     </div> <!-- outerContainer -->
 
+    <div id="floatingToolbar">
+      <button id="download" class="toolbarButton" title="Save" tabindex="31" data-l10n-id="save">
+        <span data-l10n-id="save_label">Save</span>
+      </button>
+    </div>
+
 <!--#if !MOZCENTRAL-->
     <input type="file" id="fileInput" class="hidden">
 <!--#endif-->
diff --git a/web/viewer-geckoview.js b/web/viewer-geckoview.js
index 536e37a9d..15a2d2d04 100644
--- a/web/viewer-geckoview.js
+++ b/web/viewer-geckoview.js
@@ -36,10 +36,16 @@ window.PDFViewerApplicationConstants = AppConstants;
 window.PDFViewerApplicationOptions = AppOptions;
 
 function getViewerConfiguration() {
+  const mainContainer = document.getElementById("viewerContainer");
   return {
     appContainer: document.body,
-    mainContainer: document.getElementById("viewerContainer"),
+    mainContainer,
     viewerContainer: document.getElementById("viewer"),
+    toolbar: {
+      mainContainer,
+      container: document.getElementById("floatingToolbar"),
+      download: document.getElementById("download"),
+    },
 
     passwordOverlay: {
       dialog: document.getElementById("passwordDialog"),