diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index d44b82c823..0d5ec76847 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -40,6 +40,7 @@ [app.main.ui.workspace.tokens.settings] [app.main.ui.workspace.tokens.themes.create-modal] [app.main.ui.workspace.viewport :refer [viewport*]] + [app.main.ui.workspace.webgl-unavailable-modal] [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.globals :as globals] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index a7cd731d5a..ab06a89709 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -16,7 +16,7 @@ [app.common.types.path :as path] [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] - [app.main.data.common :as dcm] + [app.main.data.modal :as modal] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.variants :as dwv] [app.main.features :as features] @@ -319,25 +319,50 @@ ;; harder to follow through. (mf/with-effect [page-id] (when-let [canvas (mf/ref-val canvas-ref)] - (->> wasm.api/module - (p/fmap (fn [ready?] - (when ready? - (let [init? (try - (wasm.api/init-canvas-context canvas) - (catch :default e - (js/console.error "Error initializing canvas context:" e) - false))] - (reset! canvas-init? init?) - (when init? - ;; Restore previous canvas pixels immediately after context initialization - ;; This happens before initialize-viewport is called - (wasm.api/apply-canvas-blur) - (wasm.api/restore-previous-canvas-pixels)) - (when-not init? - (js/alert "WebGL not supported") - (st/emit! (dcm/go-to-dashboard-recent)))))))) - (fn [] - (wasm.api/clear-canvas)))) + (let [timeout-id-ref (volatile! nil) + unmounted? (volatile! false) + modal-shown? (volatile! false) + + show-unavailable + (fn [] + (when-not (or @unmounted? @modal-shown?) + (vreset! modal-shown? true) + (reset! canvas-init? false) + (st/emit! (modal/show {:type :webgl-unavailable})))) + + try-init + (fn try-init [retries] + (when-not @unmounted? + (let [init? (try + (wasm.api/init-canvas-context canvas) + (catch :default e + (js/console.error "Error initializing canvas context:" e) + false))] + (cond + init? + (do + (reset! canvas-init? true) + ;; Restore previous canvas pixels immediately after context initialization + ;; This happens before initialize-viewport is called + (wasm.api/apply-canvas-blur) + (wasm.api/restore-previous-canvas-pixels)) + + (pos? retries) + (vreset! timeout-id-ref + (js/setTimeout #(try-init (dec retries)) 200)) + + :else + (show-unavailable)))))] + (reset! canvas-init? false) + (->> wasm.api/module + (p/fmap (fn [ready?] + (when ready? + (try-init 3))))) + (fn [] + (vreset! unmounted? true) + (when-let [timeout-id @timeout-id-ref] + (js/clearTimeout timeout-id)) + (wasm.api/clear-canvas))))) (mf/with-effect [show-text-editor? workspace-editor-state edition] (let [active-editor-state (get workspace-editor-state edition)] diff --git a/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.cljs b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.cljs new file mode 100644 index 0000000000..f4092a4762 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.cljs @@ -0,0 +1,78 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.webgl-unavailable-modal + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.common :as dcm] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.keyboard :as k] + [goog.events :as events] + [rumext.v2 :as mf]) + (:import goog.events.EventType)) + +(defn- close-and-go-dashboard + [] + (st/emit! (modal/hide) + (dcm/go-to-dashboard-recent))) + +(def ^:const webgl-troubleshooting-url "https://help.penpot.app/user-guide/first-steps/troubleshooting-webgl/") + +#_{:clojure-lsp/ignore [:clojure-lsp/unused-public-var]} +(mf/defc webgl-unavailable-modal* + {::mf/register modal/components + ::mf/register-as :webgl-unavailable} + [_] + + (let [handle-keydown (fn [event] + (when (k/esc? event) + (dom/stop-propagation event) + (close-and-go-dashboard)))] + (mf/with-effect [] + (let [key (events/listen js/document EventType.KEYDOWN handle-keydown)] + (fn [] + (events/unlistenByKey key)))) + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:header {:class (stl/css :modal-header)} + [:> icon-button* {:on-click close-and-go-dashboard + :class (stl/css :modal-close-btn) + :icon i/close + :variant "action" + :size "medium" + :aria-label (tr "labels.close")}] + [:> heading* {:level 2 :typography t/title-medium} + (tr "webgl.modals.webgl-unavailable.title")]] + + [:section {:class (stl/css :modal-content)} + [:> text* {:as "p" :typography t/body-large} + (tr "webgl.modals.webgl-unavailable.message")] + [:hr {:class (stl/css :modal-divider)}] + [:> text* {:as "p" :typography t/body-medium} + (tr "webgl.modals.webgl-unavailable.troubleshooting.before") + [:a {:href webgl-troubleshooting-url + :target "_blank" + :rel "noopener noreferrer" + :class (stl/css :link)} + (tr "webgl.modals.webgl-unavailable.troubleshooting.link")] + (tr "webgl.modals.webgl-unavailable.troubleshooting.after")]] + + [:footer {:class (stl/css :modal-footer)} + [:> button* {:on-click close-and-go-dashboard + :variant "secondary"} + (tr "webgl.modals.webgl-unavailable.cta-dashboard")] + [:> button* {:to webgl-troubleshooting-url :target "_blank" :variant "primary"} + (tr "webgl.modals.webgl-unavailable.cta-troubleshooting")]]]])) diff --git a/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss new file mode 100644 index 0000000000..0ad86b0c29 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/webgl_unavailable_modal.scss @@ -0,0 +1,61 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/_utils.scss" as *; +@use "ds/_borders.scss" as *; + +@use "refactor/common-refactor.scss" as deprecated; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-dialog { + @extend .modal-container-base; + + color: var(--color-foreground-secondary); + display: grid; + gap: var(--sp-s); + padding: px2rem(72); // FIXME: This should be a token + max-width: px2rem(682); // FIXME: This should be a token +} + +.modal-header { + color: var(--color-foreground-primary); +} + +.modal-close-btn { + position: absolute; + top: px2rem(38); // FIXME: This should be a token + right: px2rem(38); // FIXME: This should be a token +} + +.modal-content { + display: grid; + grid-template-rows: auto; + gap: var(--sp-s); + + & > * { + margin: 0; // FIXME: This should be in reset styles + } +} + +.modal-divider { + border: $b-1 solid var(--color-background-quaternary); + margin: var(--sp-xs) 0; +} + +.modal-footer { + margin-block-start: var(--sp-l); + justify-self: end; + display: flex; + flex-direction: row; + gap: var(--sp-s); +} + +.link { + color: var(--color-accent-primary); +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index a259a95a05..29efc62a11 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8907,6 +8907,27 @@ msgstr "" msgid "workspace.versions.warning.text" msgstr "Autosaved versions will be kept for %s days." +msgid "webgl.modals.webgl-unavailable.title" +msgstr "Oops! WebGL is not available" + +msgid "webgl.modals.webgl-unavailable.message" +msgstr "WebGL is not available in your browser, which is required for Penpot to work. Please check your browser settings and/or close graphics-heavy tabs." + +msgid "webgl.modals.webgl-unavailable.troubleshooting.before" +msgstr "Follow our " + +msgid "webgl.modals.webgl-unavailable.troubleshooting.link" +msgstr "WebGL troubleshooting guide" + +msgid "webgl.modals.webgl-unavailable.troubleshooting.after" +msgstr " to check browser settings, GPU acceleration, drivers, and common system issues." + +msgid "webgl.modals.webgl-unavailable.cta-dashboard" +msgstr "Go to dashboard" + +msgid "webgl.modals.webgl-unavailable.cta-troubleshooting" +msgstr "Troubleshooting guide" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 8dfcbb6c75..9314ec02d2 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8755,6 +8755,27 @@ msgstr "Si quieres aumentar este límite, contáctanos en [support@penpot.app](% msgid "workspace.versions.warning.text" msgstr "Los autoguardados duran %s días." +msgid "webgl.modals.webgl-unavailable.title" +msgstr "Vaya, WebGL no está disponible" + +msgid "webgl.modals.webgl-unavailable.message" +msgstr "WebGL no está disponible en tu navegador, y es necesario para que Penpot funcione. Revisa la configuración de tu navegador y/o cierra pestañas con uso intensivo de gráficos." + +msgid "webgl.modals.webgl-unavailable.troubleshooting.before" +msgstr "Consulta nuestra " + +msgid "webgl.modals.webgl-unavailable.troubleshooting.link" +msgstr "guía de solución de problemas de WebGL" + +msgid "webgl.modals.webgl-unavailable.troubleshooting.after" +msgstr " para revisar la configuración del navegador, la aceleración por GPU, los drivers y problemas comunes del sistema." + +msgid "webgl.modals.webgl-unavailable.cta-dashboard" +msgstr "Ir al panel" + +msgid "webgl.modals.webgl-unavailable.cta-troubleshooting" +msgstr "Guía de solución de problemas" + #, unused msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta"