mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
d8340d765a
@ -96,6 +96,7 @@
|
||||
(fn [{:keys [params path-params method] :as request}]
|
||||
(let [handler-name (:method-name path-params)
|
||||
etag (yreq/get-header request "if-none-match")
|
||||
session-id (yreq/get-header request "x-session-id")
|
||||
|
||||
key-id (get request ::http/auth-key-id)
|
||||
profile-id (or (::session/profile-id request)
|
||||
@ -108,6 +109,7 @@
|
||||
(assoc ::handler-name handler-name)
|
||||
(assoc ::ip-addr ip-addr)
|
||||
(assoc ::request-at (ct/now))
|
||||
(assoc ::session-id (some-> session-id uuid/parse*))
|
||||
(assoc ::cond/key etag)
|
||||
(cond-> (uuid? profile-id)
|
||||
(assoc ::profile-id profile-id)))
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:restore-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(let [file (bfc/get-file cfg file-id)
|
||||
team (teams/get-team conn
|
||||
@ -88,7 +88,8 @@
|
||||
;; Send to the clients a notification to reload the file
|
||||
(mbus/pub! msgbus
|
||||
:topic (:id file)
|
||||
:message {:type :file-restore
|
||||
:message {:type :file-restored
|
||||
:session-id session-id
|
||||
:file-id (:id file)
|
||||
:vern vern})
|
||||
nil)))
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
[app.common.logging :as log]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.version :as v]
|
||||
[app.util.avatars :as avatars]
|
||||
[app.util.extends]
|
||||
@ -112,10 +113,12 @@
|
||||
(def target (parse-target global))
|
||||
(def browser (parse-browser))
|
||||
(def platform (parse-platform))
|
||||
(def session-id (uuid/next))
|
||||
|
||||
(def version (parse-version global))
|
||||
(def version-tag (obj/get global "penpotVersionTag"))
|
||||
|
||||
|
||||
(defn stale-build?
|
||||
"Returns true when the compiled JS was built with a different version
|
||||
tag than the one present in the current index.html. This indicates
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.objects-map]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.event :as ev]
|
||||
@ -45,7 +44,8 @@
|
||||
(log/inf :version (:full cf/version)
|
||||
:asserts *assert*
|
||||
:build-date cf/build-date
|
||||
:public-uri (dm/str cf/public-uri))
|
||||
:public-uri (dm/str cf/public-uri)
|
||||
:session-id (str cf/session-id))
|
||||
(log/inf :hint "enabled flags" :flags (str/join " " (map name cf/flags))))
|
||||
|
||||
(declare reinit)
|
||||
@ -71,7 +71,7 @@
|
||||
(ptk/reify ::initialize
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :session-id (uuid/next)))
|
||||
(assoc state :session-id cf/session-id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
|
||||
@ -431,6 +431,7 @@
|
||||
context (-> @context
|
||||
(merge (:context event))
|
||||
(assoc :session session*)
|
||||
(assoc :session-id cf/session-id)
|
||||
(assoc :external-session-id (cf/external-session-id))
|
||||
(add-external-context-info)
|
||||
(d/without-nils))]
|
||||
|
||||
@ -224,18 +224,12 @@
|
||||
IDeref
|
||||
(-deref [_] bundle)
|
||||
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [pending-version-id (:workspace-pending-file-version-id state)
|
||||
state (-> state
|
||||
(assoc :thumbnails thumbnails)
|
||||
(update :files assoc file-id file)
|
||||
(dissoc :workspace-pending-file-version-id))]
|
||||
(cond-> state
|
||||
(some? pending-version-id)
|
||||
(assoc :workspace-file-version-id pending-version-id)
|
||||
(nil? pending-version-id)
|
||||
(dissoc :workspace-file-version-id))))))
|
||||
(-> state
|
||||
(assoc :thumbnails thumbnails)
|
||||
(update :files assoc file-id file)))))
|
||||
|
||||
(defn zoom-to-frame
|
||||
[]
|
||||
@ -253,6 +247,7 @@
|
||||
(rx/of (dws/select-shapes frames-id)
|
||||
dwz/zoom-to-selected-shape)))))
|
||||
|
||||
;; FIXME: rename to `fetch-file`
|
||||
(defn- fetch-bundle
|
||||
"Multi-stage file bundle fetch coordinator"
|
||||
[file-id features]
|
||||
@ -289,205 +284,218 @@
|
||||
;; This prevents errors when processing changes from other pages
|
||||
(when shape
|
||||
(wasm.api/process-object shape))))))
|
||||
|
||||
(defn initialize-file
|
||||
[team-id file-id]
|
||||
(assert (uuid? team-id) "expected valud uuid for `team-id`")
|
||||
(assert (uuid? file-id) "expected valud uuid for `file-id`")
|
||||
|
||||
(ptk/reify ::initialize-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [features (features/get-enabled-features state team-id)]
|
||||
(log/dbg :hint "initialize-file"
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
(rx/of (fetch-bundle file-id features))))))
|
||||
|
||||
(defn initialize-workspace
|
||||
([team-id file-id]
|
||||
(initialize-workspace team-id file-id nil))
|
||||
([team-id file-id version-id]
|
||||
(assert (uuid? team-id) "expected valud uuid for `team-id`")
|
||||
(assert (uuid? file-id) "expected valud uuid for `file-id`")
|
||||
[team-id file-id]
|
||||
(assert (uuid? team-id) "expected valud uuid for `team-id`")
|
||||
(assert (uuid? file-id) "expected valud uuid for `file-id`")
|
||||
|
||||
(ptk/reify ::initialize-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :recent-colors (:recent-colors storage/user))
|
||||
(assoc :recent-fonts (:recent-fonts storage/user))
|
||||
(assoc :current-file-id file-id)
|
||||
(assoc :workspace-presence {})
|
||||
;; Store pending version-id; bundle-fetched will set workspace-file-version-id
|
||||
;; when the new bundle is applied so the viewport re-inits with new data
|
||||
(assoc :workspace-pending-file-version-id version-id)))
|
||||
(ptk/reify ::initialize-workspace
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc :recent-colors (:recent-colors storage/user))
|
||||
(assoc :recent-fonts (:recent-fonts storage/user))
|
||||
(assoc :current-file-id file-id)
|
||||
(assoc :workspace-presence {})))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
rparams (rt/get-params state)
|
||||
features (features/get-enabled-features state team-id)
|
||||
render-wasm? (contains? features "render-wasm/v1")]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
||||
rparams (rt/get-params state)
|
||||
features (features/get-enabled-features state team-id)
|
||||
render-wasm? (contains? features "render-wasm/v1")]
|
||||
|
||||
(log/debug :hint "initialize-workspace"
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
(log/debug :hint "initialize-workspace"
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
|
||||
(rx/concat
|
||||
(->> (rx/merge
|
||||
(rx/concat
|
||||
;; Fetch all essential data that should be loaded before the file
|
||||
(rx/merge
|
||||
(if ^boolean render-wasm?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/filter true?)
|
||||
(rx/tap (fn [_]
|
||||
(let [event (ug/event "penpot:wasm:loaded")]
|
||||
(ug/dispatch! event))))
|
||||
(rx/ignore))
|
||||
(rx/empty))
|
||||
(rx/concat
|
||||
(->> (rx/merge
|
||||
(rx/concat
|
||||
;; Fetch all essential data that should be loaded before the file
|
||||
(rx/merge
|
||||
(if ^boolean render-wasm?
|
||||
(->> (rx/from @wasm/module)
|
||||
(rx/filter true?)
|
||||
(rx/tap (fn [_]
|
||||
(let [event (ug/event "penpot:wasm:loaded")]
|
||||
(ug/dispatch! event))))
|
||||
(rx/ignore))
|
||||
(rx/empty))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::df/fonts-loaded))
|
||||
(rx/take 1)
|
||||
(rx/ignore))
|
||||
|
||||
(rx/of (ntf/hide)
|
||||
(dcmt/retrieve-comment-threads file-id)
|
||||
(dcmt/fetch-profiles)
|
||||
(df/fetch-fonts team-id))
|
||||
(rx/of (ntf/hide)
|
||||
(dcmt/retrieve-comment-threads file-id)
|
||||
(dcmt/fetch-profiles)
|
||||
(df/fetch-fonts team-id))
|
||||
|
||||
(when (contains? cf/flags :mcp)
|
||||
(rx/of (du/fetch-access-tokens))))
|
||||
(when (contains? cf/flags :mcp)
|
||||
(rx/of (du/fetch-access-tokens))))
|
||||
|
||||
;; Once the essential data is fetched, lets proceed to
|
||||
;; fetch teh file bunldle
|
||||
(rx/of (fetch-bundle file-id features)))
|
||||
;; Once the essential data is fetched, lets proceed to
|
||||
;; fetch teh file bunldle
|
||||
(rx/of (initialize-file team-id file-id)))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::bundle-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [file]}]
|
||||
(log/debug :hint "bundle fetched"
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::bundle-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [file]}]
|
||||
(log/debug :hint "bundle fetched"
|
||||
:team-id (dm/str team-id)
|
||||
:file-id (dm/str file-id))
|
||||
|
||||
(rx/of (dpj/initialize-project (:project-id file))
|
||||
(dwn/initialize team-id file-id)
|
||||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id features)
|
||||
(-> (workspace-initialized file-id)
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
(rx/of (dpj/initialize-project (:project-id file))
|
||||
(dwn/initialize team-id file-id)
|
||||
(dwsl/initialize-shape-layout)
|
||||
(fetch-libraries file-id features)
|
||||
(-> (workspace-initialized file-id)
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
|
||||
;; Install dev perf observers once the workspace is ready
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/take 1)
|
||||
(rx/tap (fn [_] (perf/setup)))))
|
||||
;; Install dev perf observers once the workspace is ready
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/take 1)
|
||||
(rx/tap (fn [_] (perf/setup)))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
(rx/map dwc/set-workspace-visited))
|
||||
|
||||
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
(rx/map dwc/set-workspace-visited))
|
||||
|
||||
(when (:board-id rparams)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||
(rx/take 1)
|
||||
(rx/map zoom-to-frame)))
|
||||
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
|
||||
|
||||
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
||||
(when (:board-id rparams)
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
||||
(rx/take 1)
|
||||
(rx/map zoom-to-frame)))
|
||||
|
||||
(when render-wasm?
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [redo-changes]}]
|
||||
(let [added (->> redo-changes
|
||||
(filter #(= (:type %) :add-obj))
|
||||
(map :id))]
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/observe-on :async)
|
||||
(rx/take 1)
|
||||
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
||||
|
||||
(when render-wasm?
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(and (= :local (:source %))
|
||||
(not (contains? (:tags %) :position-data))))
|
||||
(rx/filter (complement empty?)))
|
||||
(when render-wasm?
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [redo-changes]}]
|
||||
(let [added (->> redo-changes
|
||||
(filter #(= (:type %) :add-obj))
|
||||
(map :id))]
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
|
||||
notifier-s
|
||||
(rx/merge
|
||||
(->> local-commits-s (rx/debounce 1000))
|
||||
(->> stream (rx/filter dps/force-persist?)))
|
||||
(when render-wasm?
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(and (= :local (:source %))
|
||||
(not (contains? (:tags %) :position-data))))
|
||||
(rx/filter (complement empty?)))
|
||||
|
||||
objects-s
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
notifier-s
|
||||
(rx/merge
|
||||
(->> local-commits-s (rx/debounce 1000))
|
||||
(->> stream (rx/filter dps/force-persist?)))
|
||||
|
||||
current-page-id-s
|
||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||
objects-s
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
|
||||
(->> local-commits-s
|
||||
(rx/buffer-until notifier-s)
|
||||
(rx/with-latest-from objects-s)
|
||||
(rx/map
|
||||
(fn [[commits objects]]
|
||||
(->> commits
|
||||
(mapcat :redo-changes)
|
||||
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
||||
(filter #(cfh/text-shape? objects (:id %)))
|
||||
(map #(vector
|
||||
(:id %)
|
||||
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
||||
current-page-id-s
|
||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||
|
||||
(rx/with-latest-from current-page-id-s)
|
||||
(rx/map
|
||||
(fn [[text-position-data page-id]]
|
||||
(let [changes
|
||||
(->> text-position-data
|
||||
(mapv (fn [[id position-data]]
|
||||
{:type :mod-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
:val position-data
|
||||
:ignore-touched true
|
||||
:ignore-geometry true}]})))]
|
||||
(when (d/not-empty? changes)
|
||||
(dch/commit-changes
|
||||
{:redo-changes changes :undo-changes []
|
||||
:save-undo? false
|
||||
:tags #{:position-data}})))))
|
||||
(rx/take-until stoper-s))))
|
||||
(->> local-commits-s
|
||||
(rx/buffer-until notifier-s)
|
||||
(rx/with-latest-from objects-s)
|
||||
(rx/map
|
||||
(fn [[commits objects]]
|
||||
(->> commits
|
||||
(mapcat :redo-changes)
|
||||
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
||||
(filter #(cfh/text-shape? objects (:id %)))
|
||||
(map #(vector
|
||||
(:id %)
|
||||
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
|
||||
(if (and save-undo? (seq undo-changes))
|
||||
(let [entry {:undo-changes undo-changes
|
||||
:redo-changes redo-changes
|
||||
:undo-group undo-group
|
||||
:tags tags
|
||||
:selected-before selected-before}]
|
||||
(rx/of (dwu/append-undo entry stack-undo?)))
|
||||
(rx/empty))))))
|
||||
(rx/take-until stoper-s))
|
||||
(rx/with-latest-from current-page-id-s)
|
||||
(rx/map
|
||||
(fn [[text-position-data page-id]]
|
||||
(let [changes
|
||||
(->> text-position-data
|
||||
(mapv (fn [[id position-data]]
|
||||
{:type :mod-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
:val position-data
|
||||
:ignore-touched true
|
||||
:ignore-geometry true}]})))]
|
||||
(when (d/not-empty? changes)
|
||||
(dch/commit-changes
|
||||
{:redo-changes changes :undo-changes []
|
||||
:save-undo? false
|
||||
:tags #{:position-data}})))))
|
||||
|
||||
(rx/of (mcp/notify-other-tabs-disconnect)))))
|
||||
;; FIXME: this stop-until is redundant
|
||||
(rx/take-until stoper-s))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(let [name (dm/str "workspace-" file-id)]
|
||||
(unchecked-set ug/global "name" name))))))
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/mapcat
|
||||
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
|
||||
(if (and save-undo? (seq undo-changes))
|
||||
(let [entry {:undo-changes undo-changes
|
||||
:redo-changes redo-changes
|
||||
:undo-group undo-group
|
||||
:tags tags
|
||||
:selected-before selected-before}]
|
||||
(rx/of (dwu/append-undo entry stack-undo?)))
|
||||
(rx/empty))))))
|
||||
(rx/take-until stoper-s))
|
||||
|
||||
(rx/of (mcp/notify-other-tabs-disconnect)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(let [name (dm/str "workspace-" file-id)]
|
||||
(unchecked-set ug/global "name" name)))))
|
||||
|
||||
(defn finalize-workspace
|
||||
[_team-id file-id]
|
||||
|
||||
@ -979,10 +979,7 @@
|
||||
|
||||
;; These calls are necessary for properly sync thumbnails
|
||||
;; when a main component does not live in the same page.
|
||||
;; When WASM is active, skip the "frame" tag (SVG-based) since
|
||||
;; component previews are rendered locally via WASM.
|
||||
(when-not (features/active-feature? state "render-wasm/v1")
|
||||
(update-component-thumbnail-sync state component-id file-id "frame"))
|
||||
(update-component-thumbnail-sync state component-id file-id "frame")
|
||||
(update-component-thumbnail-sync state component-id file-id "component")
|
||||
|
||||
(sync-file current-file-id file-id :components component-id undo-group)
|
||||
@ -1007,10 +1004,10 @@
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn update-component-thumbnail
|
||||
"Persist the thumbnail of the component to the server.
|
||||
For WASM, the UI is already up-to-date from the immediate render in
|
||||
update-component-thumbnail-sync, so this only persists.
|
||||
For SVG, this does the full render + persist."
|
||||
"Update the thumbnail of the component with the given id, in the
|
||||
current file and in the imported libraries.
|
||||
For WASM, re-renders and persists to the server in one step.
|
||||
For SVG, update-thumbnail already handles both render + persist."
|
||||
[component-id file-id]
|
||||
(ptk/reify ::update-component-thumbnail
|
||||
ptk/WatchEvent
|
||||
@ -1020,7 +1017,7 @@
|
||||
component (ctkl/get-component data component-id)
|
||||
page-id (:main-instance-page component)
|
||||
root-id (:main-instance-id component)]
|
||||
(rx/of (dwt.wasm/persist-thumbnail file-id page-id root-id)))
|
||||
(rx/of (dwt.wasm/render-thumbnail file-id page-id root-id :persist? true)))
|
||||
(rx/of (update-component-thumbnail-sync state component-id file-id "component"))))))
|
||||
|
||||
(defn- find-shape-index
|
||||
@ -1353,9 +1350,10 @@
|
||||
(watch [_ _ stream]
|
||||
(let [stopper-s
|
||||
(->> stream
|
||||
(rx/filter #(or (= ::dwpg/finalize-page (ptk/type %))
|
||||
(= ::watch-component-changes (ptk/type %)))))
|
||||
|
||||
(rx/map ptk/type)
|
||||
(rx/filter (fn [event-type]
|
||||
(or (= ::dwpg/finalize-page event-type)
|
||||
(= ::watch-component-changes event-type)))))
|
||||
workspace-data-s
|
||||
(->> (rx/from-atom refs/workspace-data {:emit-current-value? true})
|
||||
(rx/share))
|
||||
@ -1379,7 +1377,8 @@
|
||||
|
||||
check-changes
|
||||
(fn [[event [old-data _mid_data _new-data]]]
|
||||
(when old-data
|
||||
(if (nil? old-data)
|
||||
(rx/empty)
|
||||
(let [{:keys [file-id changes save-undo? undo-group]} event
|
||||
|
||||
changed-components
|
||||
@ -1397,18 +1396,9 @@
|
||||
(->> (rx/from changed-components)
|
||||
(rx/map #(component-changed % (:id old-data) undo-group))))
|
||||
;; even if save-undo? is false, we need to update the :modified-date of the component
|
||||
;; (for example, for undos). When WASM is active, also re-render the thumbnail
|
||||
;; so undo/redo visually updates component previews.
|
||||
(->> (mapcat (fn [component-id]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [component (ctkl/get-component old-data component-id)]
|
||||
[(touch-component component-id)
|
||||
(dwt.wasm/render-thumbnail (:id old-data)
|
||||
(:main-instance-page component)
|
||||
(:main-instance-id component))])
|
||||
[(touch-component component-id)]))
|
||||
changed-components)
|
||||
(rx/from)))
|
||||
;; (for example, for undos)
|
||||
(->> (rx/from changed-components)
|
||||
(rx/map touch-component)))
|
||||
|
||||
(rx/empty)))))
|
||||
|
||||
@ -1425,30 +1415,30 @@
|
||||
|
||||
(when (or (contains? cf/flags :component-thumbnails)
|
||||
(features/active-feature? @st/state "render-wasm/v1"))
|
||||
(let [wasm? (features/active-feature? @st/state "render-wasm/v1")]
|
||||
(->> (rx/merge
|
||||
changes-s
|
||||
(->> (rx/merge
|
||||
changes-s
|
||||
|
||||
;; WASM: render thumbnails immediately for instant UI feedback
|
||||
(if wasm?
|
||||
(->> changes-s
|
||||
(rx/filter (ptk/type? ::component-changed))
|
||||
(rx/map deref)
|
||||
(rx/map (fn [[component-id file-id]]
|
||||
(update-component-thumbnail-sync @st/state component-id file-id "component"))))
|
||||
(rx/empty))
|
||||
;; Persist thumbnails to the server in batches after user
|
||||
;; becomes inactive for 5 seconds.
|
||||
(->> changes-s
|
||||
(rx/filter (ptk/type? ::component-changed))
|
||||
(rx/map deref)
|
||||
(rx/buffer-until notifier-s)
|
||||
(rx/mapcat #(into #{} %))
|
||||
(rx/map (fn [[component-id file-id]]
|
||||
(update-component-thumbnail component-id file-id))))
|
||||
|
||||
;; Persist thumbnails to the server in batches after user
|
||||
;; becomes inactive for 5 seconds.
|
||||
(->> changes-s
|
||||
(rx/filter (ptk/type? ::component-changed))
|
||||
(rx/map deref)
|
||||
(rx/buffer-until notifier-s)
|
||||
(rx/mapcat #(into #{} %))
|
||||
(rx/map (fn [[component-id file-id]]
|
||||
(update-component-thumbnail component-id file-id)))))
|
||||
;; Immediately update the component thumbnail on undos,
|
||||
;; which emit touch-component instead of component-changed.
|
||||
(->> changes-s
|
||||
(rx/filter (ptk/type? ::touch-component))
|
||||
(rx/map deref)
|
||||
(rx/map (fn [[component-id file-id]]
|
||||
(let [file-id (or file-id (:current-file-id @st/state))]
|
||||
(update-component-thumbnail-sync
|
||||
@st/state component-id file-id "component"))))))
|
||||
|
||||
(rx/take-until stopper-s))))))))
|
||||
(rx/take-until stopper-s)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Backend interactions
|
||||
|
||||
@ -38,6 +38,23 @@
|
||||
(def ^:private xf:without-uuid-zero
|
||||
(remove #(= % uuid/zero)))
|
||||
|
||||
;; Tracks whether the WASM renderer is currently in "interactive
|
||||
;; transform" mode (a drag / resize / rotate gesture in progress).
|
||||
;; Paired with `set-modifiers-start` / `set-modifiers-end` so the
|
||||
;; native side only toggles once per gesture, regardless of how many
|
||||
;; `set-wasm-modifiers` calls fire in between.
|
||||
(defonce ^:private interactive-transform-active? (atom false))
|
||||
|
||||
(defn- ensure-interactive-transform-start!
|
||||
[]
|
||||
(when (compare-and-set! interactive-transform-active? false true)
|
||||
(wasm.api/set-modifiers-start)))
|
||||
|
||||
(defn- ensure-interactive-transform-end!
|
||||
[]
|
||||
(when (compare-and-set! interactive-transform-active? true false)
|
||||
(wasm.api/set-modifiers-end)))
|
||||
|
||||
(def ^:private transform-attrs
|
||||
#{:selrect
|
||||
:points
|
||||
@ -279,6 +296,11 @@
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "render-wasm/v1")
|
||||
;; End interactive transform mode BEFORE cleaning modifiers so
|
||||
;; the final full-quality render triggered by subsequent shape
|
||||
;; updates is not still classified as "interactive" (which would
|
||||
;; skip shadows / blur).
|
||||
(ensure-interactive-transform-end!)
|
||||
(wasm.api/clean-modifiers)
|
||||
(set-wasm-props! (dsh/lookup-page-objects state) (:wasm-props state) [])))
|
||||
|
||||
@ -624,6 +646,12 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; Entering an interactive transform (drag/resize/rotate). Flip
|
||||
;; the renderer into fast + atlas-backdrop mode so the live
|
||||
;; preview is cheap, tiles never appear sequentially and the main
|
||||
;; thread is not blocked. The pair is closed in
|
||||
;; `clear-local-transform`.
|
||||
(ensure-interactive-transform-start!)
|
||||
(wasm.api/clean-modifiers)
|
||||
(let [prev-wasm-props (:prev-wasm-props state)
|
||||
wasm-props (:wasm-props state)
|
||||
@ -764,6 +792,7 @@
|
||||
(ptk/reify ::set-wasm-rotation-modifiers
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(ensure-interactive-transform-start!)
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
ids (sequence xf-rotation-shape shapes)
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
(declare handle-pointer-update)
|
||||
(declare handle-file-change)
|
||||
(declare handle-file-deleted)
|
||||
(declare handle-file-restore)
|
||||
(declare handle-file-restored)
|
||||
(declare handle-library-change)
|
||||
(declare handle-pointer-send)
|
||||
(declare handle-export-update)
|
||||
@ -132,7 +132,7 @@
|
||||
:pointer-update (handle-pointer-update msg)
|
||||
:file-change (handle-file-change msg)
|
||||
:file-deleted (handle-file-deleted msg)
|
||||
:file-restore (handle-file-restore msg)
|
||||
:file-restored (handle-file-restored msg)
|
||||
:library-change (handle-library-change msg)
|
||||
:notification (dc/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
@ -283,22 +283,22 @@
|
||||
(rt/nav :dashboard-recent {:team-id team-id})))))))
|
||||
|
||||
(def ^:private
|
||||
schema:handle-file-restore
|
||||
[:map {:title "handle-file-restore"}
|
||||
schema:handle-file-restored
|
||||
[:map {:title "handle-file-restored"}
|
||||
[:type :keyword]
|
||||
[:file-id ::sm/uuid]
|
||||
[:vern :int]])
|
||||
|
||||
(def ^:private check-file-restore-params
|
||||
(sm/check-fn schema:handle-file-restore))
|
||||
(def ^:private check-file-restored-params
|
||||
(sm/check-fn schema:handle-file-restored))
|
||||
|
||||
(defn handle-file-restore
|
||||
(defn handle-file-restored
|
||||
[{:keys [file-id vern] :as msg}]
|
||||
|
||||
(assert (check-file-restore-params msg)
|
||||
(assert (check-file-restored-params msg)
|
||||
"expected valid parameters")
|
||||
|
||||
(ptk/reify ::handle-file-restore
|
||||
(ptk/reify ::handle-file-restored
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [curr-file-id (:current-file-id state)
|
||||
|
||||
@ -61,32 +61,34 @@
|
||||
(js/requestAnimationFrame
|
||||
(fn [_]
|
||||
(try
|
||||
(let [objects (dsh/lookup-page-objects @st/state file-id page-id)
|
||||
frame (get objects frame-id)
|
||||
{:keys [width height]} (:selrect frame)
|
||||
max-size (mth/max width height)
|
||||
scale (mth/max 1 (/ target-size max-size))
|
||||
png-bytes (wasm.api/render-shape-pixels frame-id scale)]
|
||||
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
|
||||
(do
|
||||
(l/error :hint "render-shape-pixels returned empty" :frame-id (str frame-id))
|
||||
(rx/end! subs))
|
||||
(.then
|
||||
(png-bytes->data-uri png-bytes)
|
||||
(fn [data-uri]
|
||||
(rx/push! subs data-uri)
|
||||
(rx/end! subs))
|
||||
(fn [err]
|
||||
(rx/error! subs err)))))
|
||||
(let [objects (dsh/lookup-page-objects @st/state file-id page-id)]
|
||||
(if-let [frame (get objects frame-id)]
|
||||
(let [{:keys [width height]} (:selrect frame)
|
||||
max-size (mth/max width height)
|
||||
scale (mth/max 1 (/ target-size max-size))
|
||||
png-bytes (wasm.api/render-shape-pixels frame-id scale)]
|
||||
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
|
||||
(do
|
||||
(l/error :hint "render-shape-pixels returned empty" :frame-id (str frame-id))
|
||||
(rx/end! subs))
|
||||
(.then
|
||||
(png-bytes->data-uri png-bytes)
|
||||
(fn [data-uri]
|
||||
(rx/push! subs data-uri)
|
||||
(rx/end! subs))
|
||||
(fn [err]
|
||||
(rx/error! subs err)))))
|
||||
|
||||
(rx/error! subs "Frame not found")))
|
||||
(catch :default err
|
||||
(rx/error! subs err)))))]
|
||||
#(js/cancelAnimationFrame req-id)))))
|
||||
|
||||
(defn render-thumbnail
|
||||
"Renders a component thumbnail via WASM and updates the UI immediately.
|
||||
Does NOT persist to the server — persistence is handled separately
|
||||
by `persist-thumbnail` on a debounced schedule."
|
||||
[file-id page-id frame-id]
|
||||
When `persist?` is true, also persists the rendered thumbnail to the
|
||||
server in the same observable chain (guaranteeing correct ordering)."
|
||||
[file-id page-id frame-id & {:keys [persist?] :or {persist? false}}]
|
||||
|
||||
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
|
||||
(ptk/reify ::render-thumbnail
|
||||
@ -115,15 +117,30 @@
|
||||
(catch :default err
|
||||
(rx/error! subs err)))))))
|
||||
|
||||
(persist-to-server
|
||||
[data-uri]
|
||||
(let [blob (wapi/data-uri->blob data-uri)]
|
||||
(->> (rp/cmd! :create-file-object-thumbnail
|
||||
{:file-id file-id
|
||||
:object-id object-id
|
||||
:media blob
|
||||
:tag "component"})
|
||||
(rx/catch rx/empty)
|
||||
(rx/ignore))))
|
||||
|
||||
(do-render-thumbnail
|
||||
[]
|
||||
(let [tp (ct/tpoint-ms)]
|
||||
(->> (render-component-pixels file-id page-id frame-id)
|
||||
(rx/map
|
||||
(rx/mapcat
|
||||
(fn [data-uri]
|
||||
(l/dbg :hint "component thumbnail rendered (wasm)"
|
||||
:elapsed (dm/str (tp) "ms"))
|
||||
(dwt/assoc-thumbnail object-id data-uri)))
|
||||
(if persist?
|
||||
(rx/merge
|
||||
(rx/of (dwt/assoc-thumbnail object-id data-uri))
|
||||
(persist-to-server data-uri))
|
||||
(rx/of (dwt/assoc-thumbnail object-id data-uri)))))
|
||||
|
||||
(rx/catch (fn [err]
|
||||
(js/console.error err)
|
||||
|
||||
@ -11,9 +11,10 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dwp]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.pages :as dwpg]
|
||||
[app.main.data.workspace.thumbnails :as th]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
@ -92,33 +93,59 @@
|
||||
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
|
||||
(rx/map fetch-versions)))))))
|
||||
|
||||
(defn- initialize-version
|
||||
[]
|
||||
(ptk/reify ::initialize-version
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
file-id (:current-file-id state)
|
||||
team-id (:current-team-id state)]
|
||||
|
||||
(rx/merge
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dw/bundle-fetched))
|
||||
(rx/take 1)
|
||||
(rx/map #(dwpg/initialize-page file-id page-id)))
|
||||
|
||||
(rx/of (ntf/hide :tag :restore-dialog)
|
||||
(dw/initialize-file team-id file-id)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(th/clear-queue!))))
|
||||
|
||||
(defn- wait-for-persistence
|
||||
[file-id snapshot-id]
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id snapshot-id}))))
|
||||
|
||||
(defn restore-version
|
||||
[id origin]
|
||||
(assert (uuid? id) "expected valid uuid for `id`")
|
||||
(ptk/reify ::restore-version
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
team-id (:current-team-id state)]
|
||||
(let [file-id (:current-file-id state)
|
||||
team-id (:current-team-id state)
|
||||
event-name (case origin
|
||||
:version "restore-pin-version"
|
||||
:snapshot "restore-autosave"
|
||||
:plugin "restore-version-plugin")]
|
||||
|
||||
(rx/concat
|
||||
(rx/of ::dwp/force-persist
|
||||
(dw/remove-layout-flag :document-history))
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/tap #(th/clear-queue!))
|
||||
(rx/map #(dw/initialize-workspace team-id file-id id)))
|
||||
(case origin
|
||||
:version
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
|
||||
|
||||
:snapshot
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"}))
|
||||
|
||||
:plugin
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}))
|
||||
(->> (wait-for-persistence file-id id)
|
||||
(rx/map #(initialize-version)))
|
||||
|
||||
(if event-name
|
||||
(rx/of (ev/event {::ev/name event-name
|
||||
:file-id file-id
|
||||
:team-id team-id}))
|
||||
(rx/empty)))))))
|
||||
|
||||
(defn delete-version
|
||||
@ -220,18 +247,15 @@
|
||||
(ptk/reify ::restore-version-from-plugins
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file (dsh/lookup-file state file-id)
|
||||
team-id (or (:team-id file) (:current-file-id state))]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(rx/concat
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"})
|
||||
(rx/of (ev/event {::ev/name "restore-version-plugin"
|
||||
:file-id file-id
|
||||
:team-id team-id})
|
||||
::dwp/force-persist)
|
||||
|
||||
;; FIXME: we should abstract this
|
||||
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
|
||||
(rx/filter #(or (nil? %) (= :saved %)))
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/map #(dw/initialize-workspace team-id file-id id)))
|
||||
(->> (wait-for-persistence file-id id)
|
||||
(rx/map #(initialize-version)))
|
||||
|
||||
(->> (rx/of 1)
|
||||
(rx/tap resolve)
|
||||
|
||||
@ -428,52 +428,71 @@
|
||||
(ex/print-throwable instance :prefix "Server Error"))
|
||||
(st/async-emit! (rt/assign-exception error)))
|
||||
|
||||
(defn- from-extension?
|
||||
"True when the error stack trace originates from a browser extension."
|
||||
[cause]
|
||||
(let [stack (.-stack cause)]
|
||||
(and (string? stack)
|
||||
(or (str/includes? stack "chrome-extension://")
|
||||
(str/includes? stack "moz-extension://")))))
|
||||
|
||||
(defn- from-posthog?
|
||||
"True when the error stack trace originates from PostHog analytics."
|
||||
[cause]
|
||||
(let [stack (.-stack cause)]
|
||||
(and (string? stack)
|
||||
(str/includes? stack "posthog"))))
|
||||
|
||||
(defn is-ignorable-exception?
|
||||
"True when the error is known to be harmless (browser extensions, analytics,
|
||||
React/extension DOM conflicts, etc.) and should NOT be surfaced to the user."
|
||||
[cause]
|
||||
(let [message (ex-message cause)]
|
||||
(or (from-extension? cause)
|
||||
(from-posthog? cause)
|
||||
(= message "Possible side-effect in debug-evaluate")
|
||||
(= message "Unexpected end of input")
|
||||
(str/starts-with? message "invalid props on component")
|
||||
(str/starts-with? message "Unexpected token ")
|
||||
;; Native AbortError DOMException: raised when an in-flight
|
||||
;; HTTP fetch is cancelled via AbortController (e.g. by an
|
||||
;; RxJS unsubscription / take-until chain). These are
|
||||
;; handled gracefully inside app.util.http/fetch and must NOT
|
||||
;; be surfaced as application errors.
|
||||
(= (.-name ^js cause) "AbortError")
|
||||
;; Zone.js (injected by browser extensions such as Angular
|
||||
;; DevTools) wraps event listeners and assigns a custom
|
||||
;; .toString to its wrapper functions using
|
||||
;; Object.defineProperty. When the wrapper was previously
|
||||
;; defined with {writable: false}, a subsequent plain assignment
|
||||
;; in strict mode (our libs.js uses "use strict") throws this
|
||||
;; TypeError. This is a known Zone.js / browser-extension
|
||||
;; incompatibility and is NOT a Penpot bug.
|
||||
(str/starts-with? message "Cannot assign to read only property 'toString'")
|
||||
;; NotFoundError DOMException: "Failed to execute
|
||||
;; 'removeChild' on 'Node'" — Thrown by React's commit
|
||||
;; phase when the DOM tree has been modified externally
|
||||
;; (typically by browser extensions like Grammarly,
|
||||
;; LastPass, translation tools, or ad blockers that
|
||||
;; inject/remove nodes). The entire stack trace is inside
|
||||
;; React internals (libs.js) with no application code,
|
||||
;; so there is nothing actionable on our side. React's
|
||||
;; error boundary already handles recovery.
|
||||
(and (= (.-name ^js cause) "NotFoundError")
|
||||
(str/includes? message "removeChild")))))
|
||||
|
||||
(defn- from-plugin?
|
||||
"Check if the error is marked as originating from plugin code. The
|
||||
plugin runtime tracks plugin errors in a WeakMap, which works even
|
||||
in SES hardened environments where error objects may be frozen."
|
||||
[cause]
|
||||
(try
|
||||
(is-plugin-error? cause)
|
||||
(catch :default _
|
||||
false)))
|
||||
|
||||
(defonce uncaught-error-handler
|
||||
(letfn [(from-extension? [cause]
|
||||
(let [stack (.-stack cause)]
|
||||
(and (string? stack)
|
||||
(or (str/includes? stack "chrome-extension://")
|
||||
(str/includes? stack "moz-extension://")))))
|
||||
|
||||
(from-posthog? [cause]
|
||||
(let [stack (.-stack cause)]
|
||||
(and (string? stack)
|
||||
(str/includes? stack "posthog"))))
|
||||
|
||||
;; Check if the error is marked as originating from plugin code.
|
||||
;; The plugin runtime tracks plugin errors in a WeakMap, which works
|
||||
;; even in SES hardened environments where error objects may be frozen.
|
||||
(from-plugin? [cause]
|
||||
(try
|
||||
(is-plugin-error? cause)
|
||||
(catch :default _
|
||||
false)))
|
||||
|
||||
(is-ignorable-exception? [cause]
|
||||
(let [message (ex-message cause)]
|
||||
(or (from-extension? cause)
|
||||
(from-posthog? cause)
|
||||
(= message "Possible side-effect in debug-evaluate")
|
||||
(= message "Unexpected end of input")
|
||||
(str/starts-with? message "invalid props on component")
|
||||
(str/starts-with? message "Unexpected token ")
|
||||
;; Native AbortError DOMException: raised when an in-flight
|
||||
;; HTTP fetch is cancelled via AbortController (e.g. by an
|
||||
;; RxJS unsubscription / take-until chain). These are
|
||||
;; handled gracefully inside app.util.http/fetch and must NOT
|
||||
;; be surfaced as application errors.
|
||||
(= (.-name ^js cause) "AbortError")
|
||||
;; Zone.js (injected by browser extensions such as Angular
|
||||
;; DevTools) wraps event listeners and assigns a custom
|
||||
;; .toString to its wrapper functions using
|
||||
;; Object.defineProperty. When the wrapper was previously
|
||||
;; defined with {writable: false}, a subsequent plain assignment
|
||||
;; in strict mode (our libs.js uses "use strict") throws this
|
||||
;; TypeError. This is a known Zone.js / browser-extension
|
||||
;; incompatibility and is NOT a Penpot bug.
|
||||
(str/starts-with? message "Cannot assign to read only property 'toString'"))))
|
||||
|
||||
(on-unhandled-error [event]
|
||||
(letfn [(on-unhandled-error [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "error")]
|
||||
(cond
|
||||
|
||||
@ -259,9 +259,6 @@
|
||||
(def workspace-layout
|
||||
(l/derived :workspace-layout st/state))
|
||||
|
||||
(def workspace-file-version-id
|
||||
(l/derived :workspace-file-version-id st/state))
|
||||
|
||||
(def snap-pixel?
|
||||
(l/derived #(contains? % :snap-pixel-grid) workspace-layout))
|
||||
|
||||
|
||||
@ -182,6 +182,7 @@
|
||||
:credentials "include"
|
||||
:headers {"accept" "application/transit+json,text/event-stream,*/*"
|
||||
"x-external-session-id" (cf/external-session-id)
|
||||
"x-session-id" (str cf/session-id)
|
||||
"x-event-origin" (::ev/origin (meta params))}
|
||||
:body (when (= method :post)
|
||||
(if form-data?
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
|
||||
(mf/defc workspace-content*
|
||||
{::mf/private true}
|
||||
[{:keys [file layout page wglobal file-version-id]}]
|
||||
[{:keys [file layout page wglobal]}]
|
||||
|
||||
(let [palete-size (mf/use-state nil)
|
||||
selected (mf/deref refs/selected-shapes)
|
||||
@ -110,7 +110,6 @@
|
||||
:wglobal wglobal
|
||||
:selected selected
|
||||
:layout layout
|
||||
:file-version-id file-version-id
|
||||
:palete-size
|
||||
(when (and (or colorpalette? textpalette?) (not hide-ui?))
|
||||
@palete-size)}]]]
|
||||
@ -170,7 +169,7 @@
|
||||
|
||||
(mf/defc workspace-inner*
|
||||
{::mf/private true}
|
||||
[{:keys [page-id file-id file layout wglobal file-version-id]}]
|
||||
[{:keys [page-id file-id file layout wglobal]}]
|
||||
(let [page-ref (mf/with-memo [file-id page-id]
|
||||
(make-page-ref file-id page-id))
|
||||
page (mf/deref page-ref)]
|
||||
@ -189,8 +188,7 @@
|
||||
[:> workspace-content* {:file file
|
||||
:page page
|
||||
:wglobal wglobal
|
||||
:layout layout
|
||||
:file-version-id file-version-id}]
|
||||
:layout layout}]
|
||||
[:> workspace-loader*])))
|
||||
|
||||
(mf/defc workspace*
|
||||
@ -202,7 +200,6 @@
|
||||
|
||||
layout (mf/deref refs/workspace-layout)
|
||||
wglobal (mf/deref refs/workspace-global)
|
||||
file-version-id (mf/deref refs/workspace-file-version-id)
|
||||
|
||||
team-ref (mf/with-memo [team-id]
|
||||
(make-team-ref team-id))
|
||||
@ -278,8 +275,7 @@
|
||||
:file-id file-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout
|
||||
:file-version-id file-version-id}])
|
||||
:layout layout}])
|
||||
(when (or (not (and file-loaded? page-id))
|
||||
;; in wasm renderer, extend the pixel loader until the first frame is rendered
|
||||
;; but do not apply it when switching pages
|
||||
|
||||
@ -265,54 +265,68 @@
|
||||
prev-transforms (mf/use-var nil)]
|
||||
|
||||
(mf/with-effect [add-children]
|
||||
(ts/raf
|
||||
#(doseq [{:keys [shape]} add-children-prev]
|
||||
(let [shape-node (get-shape-node shape)
|
||||
mirror-node (dom/query (dm/fmt ".mirror-shape[href='#shape-%'" shape))]
|
||||
(when mirror-node (.remove mirror-node))
|
||||
(dom/remove-attribute! (dom/get-parent shape-node) "display"))))
|
||||
(let [raf-id1
|
||||
(ts/raf
|
||||
#(doseq [{:keys [shape]} add-children-prev]
|
||||
(let [shape-node (get-shape-node shape)
|
||||
mirror-node (dom/query (dm/fmt ".mirror-shape[href='#shape-%'" shape))]
|
||||
(when mirror-node (.remove mirror-node))
|
||||
(when-let [parent (some-> shape-node dom/get-parent)]
|
||||
(dom/remove-attribute! parent "display")))))
|
||||
|
||||
(ts/raf
|
||||
#(doseq [{:keys [frame shape]} add-children]
|
||||
(let [frame-node (get-shape-node frame)
|
||||
shape-node (get-shape-node shape)
|
||||
raf-id2
|
||||
(ts/raf
|
||||
#(doseq [{:keys [frame shape]} add-children]
|
||||
(let [frame-node (get-shape-node frame)
|
||||
shape-node (get-shape-node shape)]
|
||||
(when (and (some? frame-node) (some? shape-node))
|
||||
(let [clip-id
|
||||
(-> (dom/query frame-node ":scope > defs > .frame-clip-def")
|
||||
(dom/get-attribute "id"))
|
||||
|
||||
clip-id
|
||||
(-> (dom/query frame-node ":scope > defs > .frame-clip-def")
|
||||
(dom/get-attribute "id"))
|
||||
use-node
|
||||
(dom/create-element "http://www.w3.org/2000/svg" "use")
|
||||
|
||||
use-node
|
||||
(dom/create-element "http://www.w3.org/2000/svg" "use")
|
||||
contents-node
|
||||
(or (dom/query frame-node ".frame-children") frame-node)]
|
||||
|
||||
contents-node
|
||||
(or (dom/query frame-node ".frame-children") frame-node)]
|
||||
|
||||
(dom/set-attribute! use-node "href" (dm/fmt "#shape-%" shape))
|
||||
(dom/set-attribute! use-node "clip-path" (dm/fmt "url(#%)" clip-id))
|
||||
(dom/add-class! use-node "mirror-shape")
|
||||
(dom/append-child! contents-node use-node)
|
||||
(dom/set-attribute! (dom/get-parent shape-node) "display" "none")))))
|
||||
(dom/set-attribute! use-node "href" (dm/fmt "#shape-%" shape))
|
||||
(dom/set-attribute! use-node "clip-path" (dm/fmt "url(#%)" clip-id))
|
||||
(dom/add-class! use-node "mirror-shape")
|
||||
(dom/append-child! contents-node use-node)
|
||||
(dom/set-attribute! (dom/get-parent shape-node) "display" "none"))))))]
|
||||
(fn []
|
||||
(js/cancelAnimationFrame raf-id1)
|
||||
(js/cancelAnimationFrame raf-id2))))
|
||||
|
||||
(mf/with-effect [transforms]
|
||||
(let [curr-shapes-set (into #{} (map :id) shapes)
|
||||
prev-shapes-set (into #{} (map :id) @prev-shapes)
|
||||
|
||||
new-shapes (->> shapes (remove #(contains? prev-shapes-set (:id %))))
|
||||
removed-shapes (->> @prev-shapes (remove #(contains? curr-shapes-set (:id %))))]
|
||||
removed-shapes (->> @prev-shapes (remove #(contains? curr-shapes-set (:id %))))
|
||||
|
||||
;; NOTE: we schedule the dom modifications to be executed
|
||||
;; asynchronously for avoid component flickering when react18
|
||||
;; is used.
|
||||
;; NOTE: we schedule the dom modifications to be executed
|
||||
;; asynchronously for avoid component flickering when react18
|
||||
;; is used.
|
||||
|
||||
(when (d/not-empty? new-shapes)
|
||||
(ts/raf #(start-transform! node new-shapes)))
|
||||
raf-id1
|
||||
(when (d/not-empty? new-shapes)
|
||||
(ts/raf #(start-transform! node new-shapes)))
|
||||
|
||||
(when (d/not-empty? shapes)
|
||||
(ts/raf #(update-transform! node shapes transforms modifiers)))
|
||||
raf-id2
|
||||
(when (d/not-empty? shapes)
|
||||
(ts/raf #(update-transform! node shapes transforms modifiers)))
|
||||
|
||||
(when (d/not-empty? removed-shapes)
|
||||
(ts/raf #(remove-transform! node removed-shapes))))
|
||||
raf-id3
|
||||
(when (d/not-empty? removed-shapes)
|
||||
(ts/raf #(remove-transform! node removed-shapes)))]
|
||||
|
||||
(reset! prev-modifiers modifiers)
|
||||
(reset! prev-transforms transforms)
|
||||
(reset! prev-shapes shapes))))
|
||||
(reset! prev-modifiers modifiers)
|
||||
(reset! prev-transforms transforms)
|
||||
(reset! prev-shapes shapes)
|
||||
|
||||
(fn []
|
||||
(when raf-id1 (js/cancelAnimationFrame raf-id1))
|
||||
(when raf-id2 (js/cancelAnimationFrame raf-id2))
|
||||
(when raf-id3 (js/cancelAnimationFrame raf-id3)))))))
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.thumbnails-wasm :as dwt.wasm]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.features :as features]
|
||||
@ -242,7 +243,11 @@
|
||||
;; afterwards, in the next render cycle.
|
||||
(dom/append-child! item-el counter-el)
|
||||
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
|
||||
(ts/raf #(.removeChild ^js item-el counter-el))))
|
||||
;; Guard against race condition: if the user navigates away
|
||||
;; before the RAF fires, item-el may have been unmounted and
|
||||
;; counter-el is no longer a child — removeChild would throw.
|
||||
(ts/raf #(when (dom/child? counter-el item-el)
|
||||
(dom/remove-child! item-el counter-el)))))
|
||||
|
||||
(defn on-asset-drag-start
|
||||
[event file-id asset selected item-ref asset-type on-drag-start]
|
||||
@ -282,6 +287,9 @@
|
||||
(let [page-id (:main-instance-page component)
|
||||
root-id (:main-instance-id component)
|
||||
retry (mf/use-state 0)
|
||||
wasm? (features/active-feature? @st/state "render-wasm/v1")
|
||||
current-page-id (mf/deref refs/current-page-id)
|
||||
thumbnail-requested? (mf/use-ref false)
|
||||
|
||||
thumbnail-uri*
|
||||
(mf/with-memo [file-id page-id root-id]
|
||||
@ -298,9 +306,23 @@
|
||||
(when (< @retry 3)
|
||||
(inc retry))))]
|
||||
|
||||
;; Lazy WASM thumbnail rendering: when the component becomes
|
||||
;; visible, has no cached thumbnail, and lives on the current page
|
||||
;; trigger a render. Ref is used to avoid triggering multiple renders
|
||||
;; while the component is still not rendered and the thumbnail URI
|
||||
;; is not available.
|
||||
(mf/use-effect
|
||||
(mf/deps is-hidden thumbnail-uri wasm? current-page-id file-id page-id)
|
||||
(fn []
|
||||
(if (some? thumbnail-uri)
|
||||
(mf/set-ref-val! thumbnail-requested? false)
|
||||
(when (and wasm? (not is-hidden) (not (mf/ref-val thumbnail-requested?)) (= page-id current-page-id))
|
||||
(mf/set-ref-val! thumbnail-requested? true)
|
||||
(st/emit! (dwt.wasm/render-thumbnail file-id page-id root-id))))))
|
||||
|
||||
(if (and (some? thumbnail-uri)
|
||||
(or (contains? cf/flags :component-thumbnails)
|
||||
(features/active-feature? @st/state "render-wasm/v1")))
|
||||
wasm?))
|
||||
[:& component-svg-thumbnail
|
||||
{:thumbnail-uri thumbnail-uri
|
||||
:class class
|
||||
|
||||
@ -589,7 +589,7 @@
|
||||
:on-click on-select
|
||||
:disabled loop}
|
||||
(when visible?
|
||||
[:> cmm/component-item-thumbnail* {:file-id (:file-id item)
|
||||
[:> cmm/component-item-thumbnail* {:file-id file-id
|
||||
:class (stl/css :swap-item-thumbnail)
|
||||
:root-shape root-shape
|
||||
:component item
|
||||
|
||||
@ -531,16 +531,18 @@
|
||||
[:map
|
||||
[:values schema:layout-item-props-schema]
|
||||
[:applied-tokens [:maybe [:map-of :keyword :string]]]
|
||||
[:ids [::sm/vec ::sm/uuid]]
|
||||
[:v-sizing {:optional true} [:maybe [:enum :fill :fix :auto]]]])
|
||||
[:ids [::sm/vec ::sm/uuid]]])
|
||||
|
||||
(mf/defc layout-size-constraints*
|
||||
{::mf/private true
|
||||
::mf/schema (sm/schema schema:layout-size-constraints)}
|
||||
[{:keys [values v-sizing ids applied-tokens] :as props}]
|
||||
[{:keys [values ids applied-tokens] :as props}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
v-sizing
|
||||
(:layout-item-v-sizing values)
|
||||
|
||||
min-w (get values :layout-item-min-w)
|
||||
|
||||
max-w (get values :layout-item-max-w)
|
||||
@ -904,5 +906,4 @@
|
||||
(= v-sizing :fill))
|
||||
[:> layout-size-constraints* {:ids ids
|
||||
:values values
|
||||
:applied-tokens applied-tokens
|
||||
:v-sizing v-sizing}])])]))
|
||||
:applied-tokens applied-tokens}])])]))
|
||||
|
||||
@ -81,6 +81,7 @@
|
||||
selected))
|
||||
|
||||
(mf/defc viewport-classic*
|
||||
{::mf/private true}
|
||||
[{:keys [selected wglobal layout file page palete-size]}]
|
||||
(let [{:keys [edit-path
|
||||
panning
|
||||
@ -108,8 +109,8 @@
|
||||
;; DEREFS
|
||||
drawing (mf/deref refs/workspace-drawing)
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
|
||||
file-id (get file :id)
|
||||
vern (get file :vern)
|
||||
page-id (get page :id)
|
||||
objects (get page :objects)
|
||||
background (get page :background clr/canvas)
|
||||
@ -341,7 +342,7 @@
|
||||
:opacity 0.6}}
|
||||
(when (and (:can-edit permissions) (not read-only?))
|
||||
[:& stvh/viewport-texts
|
||||
{:key (dm/str "texts-" page-id)
|
||||
{:key (dm/str "viewport-texts-" page-id "-" vern)
|
||||
:page-id page-id
|
||||
:objects objects
|
||||
:modifiers modifiers
|
||||
@ -367,7 +368,7 @@
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:xmlns:penpot "https://penpot.app/xmlns"
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:key (str "render" page-id)
|
||||
:key (dm/str "viewport-svg-" page-id "-" vern)
|
||||
:width (:width vport 0)
|
||||
:height (:height vport 0)
|
||||
:view-box (utils/format-viewbox vbox)
|
||||
@ -401,7 +402,7 @@
|
||||
[:& (mf/provider ctx/current-vbox) {:value vbox'}
|
||||
[:& (mf/provider use/include-metadata-ctx) {:value (dbg/enabled? :show-export-metadata)}
|
||||
;; Render root shape
|
||||
[:& shapes/root-shape {:key page-id
|
||||
[:& shapes/root-shape {:key (str page-id)
|
||||
:objects base-objects
|
||||
:active-frames @active-frames}]]]]
|
||||
|
||||
@ -409,7 +410,7 @@
|
||||
{:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:key (str "viewport" page-id)
|
||||
:key (dm/str "viewport-controls-" page-id "-" vern)
|
||||
:view-box (utils/format-viewbox vbox)
|
||||
:ref on-viewport-ref
|
||||
:class (dm/str @cursor (when drawing-tool " drawing") " " (stl/css :viewport-controls))
|
||||
@ -720,7 +721,7 @@
|
||||
(not= @hover-top-frame-id (:id frame)))
|
||||
[:& grid-layout/editor
|
||||
{:zoom zoom
|
||||
:key (dm/str (:id frame))
|
||||
:key (dm/str "viewport-frame-" (:id frame))
|
||||
:objects base-objects
|
||||
:modifiers modifiers
|
||||
:shape frame
|
||||
@ -734,8 +735,11 @@
|
||||
:bottom-padding (when palete-size (+ palete-size 8))}]]]]]))
|
||||
|
||||
(mf/defc viewport*
|
||||
[props]
|
||||
(let [wasm-renderer-enabled? (features/use-feature "render-wasm/v1")]
|
||||
(if ^boolean wasm-renderer-enabled?
|
||||
[:> viewport.wasm/viewport* props]
|
||||
[:> viewport-classic* props])))
|
||||
[{:keys [file page] :as props}]
|
||||
(let [vern (get file :vern)
|
||||
page-id (get page :id)
|
||||
render-wasm? (features/use-feature "render-wasm/v1")]
|
||||
[:* {:key (dm/str "viewport-" page-id "-" vern)}
|
||||
(if ^boolean render-wasm?
|
||||
[:> viewport.wasm/viewport* props]
|
||||
[:> viewport-classic* props])]))
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
(apply-modifiers-to-objects objects (select-keys (into {} modifiers) selected)))
|
||||
|
||||
(mf/defc viewport*
|
||||
[{:keys [selected wglobal layout file page palete-size file-version-id]}]
|
||||
[{:keys [selected wglobal layout file page palete-size]}]
|
||||
(let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
|
||||
;; that the new parameter is sent
|
||||
|
||||
@ -111,6 +111,7 @@
|
||||
workspace-editor-state (mf/deref refs/workspace-editor-state)
|
||||
|
||||
file-id (get file :id)
|
||||
vern (get file :vern)
|
||||
objects (get page :objects)
|
||||
page-id (get page :id)
|
||||
background (get page :background clr/canvas)
|
||||
@ -154,7 +155,7 @@
|
||||
|
||||
canvas-ref (mf/use-ref nil)
|
||||
text-editor-ref (mf/use-ref nil)
|
||||
last-file-version-id-ref (mf/use-ref nil)
|
||||
last-vern-ref (mf/use-ref nil)
|
||||
|
||||
;; STATE REFS
|
||||
disable-paste-ref (mf/use-ref false)
|
||||
@ -393,10 +394,11 @@
|
||||
(when (and @canvas-init? preview-blend)
|
||||
(wasm.api/request-render "with-effect")))
|
||||
|
||||
(mf/with-effect [@canvas-init? file-version-id zoom vbox background]
|
||||
(mf/with-effect [@canvas-init? vern zoom vbox background]
|
||||
(when @canvas-init?
|
||||
(if (not @initialized?)
|
||||
(do
|
||||
(mf/set-ref-val! last-vern-ref vern)
|
||||
;; Initial file open uses the same transition workflow as page switches,
|
||||
;; but with a solid background-color blurred placeholder.
|
||||
(wasm.api/start-initial-load-transition! background)
|
||||
@ -404,14 +406,12 @@
|
||||
;; blank canvas (first load) visible while shapes load.
|
||||
;; The loading overlay is suppressed because on-shapes-ready
|
||||
;; is set.
|
||||
(wasm.api/initialize-viewport
|
||||
base-objects zoom vbox :background background)
|
||||
(reset! initialized? true)
|
||||
(mf/set-ref-val! last-file-version-id-ref file-version-id))
|
||||
(when (and (some? file-version-id)
|
||||
(not= file-version-id (mf/ref-val last-file-version-id-ref)))
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox :background background)
|
||||
(mf/set-ref-val! last-file-version-id-ref file-version-id)))))
|
||||
(reset! initialized? true))
|
||||
|
||||
(when (and (some? vern) (not= vern (mf/ref-val last-vern-ref)))
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox :background background)
|
||||
(mf/set-ref-val! last-vern-ref vern)))))
|
||||
|
||||
(mf/with-effect [focus]
|
||||
(when (and @canvas-init? @initialized?)
|
||||
|
||||
@ -1518,6 +1518,23 @@
|
||||
[]
|
||||
(h/call wasm/internal-module "_clean_modifiers"))
|
||||
|
||||
(defn set-modifiers-start
|
||||
"Enter interactive transform mode (drag / resize / rotate). Enables
|
||||
fast-mode effect skipping in the renderer and activates an atlas
|
||||
backdrop so tiles do not appear sequentially or flicker while the
|
||||
gesture is in progress."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_set_modifiers_start")))
|
||||
|
||||
(defn set-modifiers-end
|
||||
"Leave interactive transform mode. Cancels any pending async render
|
||||
scheduled under it; the caller is expected to trigger a full-quality
|
||||
render (via `request-render`) once the gesture is committed."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_set_modifiers_end")))
|
||||
|
||||
(defn set-modifiers
|
||||
[modifiers]
|
||||
|
||||
@ -1583,6 +1600,42 @@
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-blur-downscale-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:blur_downscale_threshold p))]
|
||||
(let [n (if (string? raw) (js/parseFloat raw) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-max-blocking-time-ms-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:max_blocking_time_ms p))]
|
||||
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-node-batch-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:node_batch_threshold p))]
|
||||
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn- wasm-viewport-interest-area-threshold-from-route-params
|
||||
"Reads optional `aa_threshold` query param from the router"
|
||||
[]
|
||||
(when-let [raw (let [p (rt/get-params @st/state)]
|
||||
(:viewport_interest_area_threshold p))]
|
||||
(let [n (if (string? raw) (js/parseInt raw 10) raw)]
|
||||
(when (and (number? n) (not (js/isNaN n)) (pos? n))
|
||||
n))))
|
||||
|
||||
(defn set-canvas-size
|
||||
[canvas]
|
||||
(let [width (or (.-clientWidth ^js canvas) (.-width ^js canvas))
|
||||
@ -1620,6 +1673,14 @@
|
||||
(h/call wasm/internal-module "_set_render_options" flags dpr)
|
||||
(when-let [t (wasm-aa-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_antialias_threshold" t))
|
||||
(when-let [t (wasm-viewport-interest-area-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_viewport_interest_area_threshold" t))
|
||||
(when-let [t (wasm-max-blocking-time-ms-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_max_blocking_time_ms" t))
|
||||
(when-let [t (wasm-node-batch-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_node_batch_threshold" t))
|
||||
(when-let [t (wasm-blur-downscale-threshold-from-route-params)]
|
||||
(h/call wasm/internal-module "_set_blur_downscale_threshold" t))
|
||||
(when-let [max-tex (webgl/max-texture-size context)]
|
||||
(h/call wasm/internal-module "_set_max_atlas_texture_size" max-tex))
|
||||
|
||||
|
||||
@ -121,24 +121,25 @@
|
||||
;; IMPORTANT: Only TTF fonts can be stored.
|
||||
(defn- store-font-buffer
|
||||
[font-data font-array-buffer emoji? fallback?]
|
||||
(let [font-id-buffer (:family-id-buffer font-data)
|
||||
size (.-byteLength font-array-buffer)
|
||||
ptr (h/call wasm/internal-module "_alloc_bytes" size)
|
||||
heap (gobj/get ^js wasm/internal-module "HEAPU8")
|
||||
mem (js/Uint8Array. (.-buffer heap) ptr size)]
|
||||
(when wasm/context-initialized?
|
||||
(let [font-id-buffer (:family-id-buffer font-data)
|
||||
size (.-byteLength font-array-buffer)
|
||||
ptr (h/call wasm/internal-module "_alloc_bytes" size)
|
||||
heap (gobj/get ^js wasm/internal-module "HEAPU8")
|
||||
mem (js/Uint8Array. (.-buffer heap) ptr size)]
|
||||
|
||||
(.set mem (js/Uint8Array. font-array-buffer))
|
||||
(st/emit! (ptk/data-event :font-loaded {:font-id (:font-id font-data)}))
|
||||
(h/call wasm/internal-module "_store_font"
|
||||
(aget font-id-buffer 0)
|
||||
(aget font-id-buffer 1)
|
||||
(aget font-id-buffer 2)
|
||||
(aget font-id-buffer 3)
|
||||
(:weight font-data)
|
||||
(:style font-data)
|
||||
emoji?
|
||||
fallback?)
|
||||
true))
|
||||
(.set mem (js/Uint8Array. font-array-buffer))
|
||||
(st/emit! (ptk/data-event :font-loaded {:font-id (:font-id font-data)}))
|
||||
(h/call wasm/internal-module "_store_font"
|
||||
(aget font-id-buffer 0)
|
||||
(aget font-id-buffer 1)
|
||||
(aget font-id-buffer 2)
|
||||
(aget font-id-buffer 3)
|
||||
(:weight font-data)
|
||||
(:style font-data)
|
||||
emoji?
|
||||
fallback?)
|
||||
true)))
|
||||
|
||||
;; Tracks fonts currently being fetched: {url -> fallback?}
|
||||
;; When the same font is requested as both primary and fallback,
|
||||
|
||||
95
frontend/test/frontend_tests/errors_test.cljs
Normal file
95
frontend/test/frontend_tests/errors_test.cljs
Normal file
@ -0,0 +1,95 @@
|
||||
;; 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 frontend-tests.errors-test
|
||||
(:require
|
||||
[app.main.errors :as errors]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- make-error
|
||||
"Create a JS Error-like object with the given name, message, and optional stack."
|
||||
[error-name message & {:keys [stack] :or {stack ""}}]
|
||||
(let [err (js/Error. message)]
|
||||
(set! (.-name err) error-name)
|
||||
(when (some? stack)
|
||||
(set! (.-stack err) stack))
|
||||
err))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; is-ignorable-exception? tests
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest test-ignorable-chrome-extension
|
||||
(t/testing "Errors from Chrome extensions are ignorable"
|
||||
(let [cause (make-error "Error" "some error"
|
||||
:stack "Error: some error\n at chrome-extension://abc123/content.js:1:1")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-moz-extension
|
||||
(t/testing "Errors from Firefox extensions are ignorable"
|
||||
(let [cause (make-error "Error" "some error"
|
||||
:stack "Error: some error\n at moz-extension://abc123/content.js:1:1")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-posthog
|
||||
(t/testing "Errors from PostHog are ignorable"
|
||||
(let [cause (make-error "Error" "some error"
|
||||
:stack "Error: some error\n at https://app.posthog.com/static/array.js:1:1")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-debug-evaluate
|
||||
(t/testing "Debug-evaluate side-effect errors are ignorable"
|
||||
(let [cause (make-error "Error" "Possible side-effect in debug-evaluate")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-unexpected-end-of-input
|
||||
(t/testing "Unexpected end of input errors are ignorable"
|
||||
(let [cause (make-error "SyntaxError" "Unexpected end of input")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-invalid-props
|
||||
(t/testing "Invalid React props errors are ignorable"
|
||||
(let [cause (make-error "Error" "invalid props on component Foo")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-unexpected-token
|
||||
(t/testing "Unexpected token errors are ignorable"
|
||||
(let [cause (make-error "SyntaxError" "Unexpected token <")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-abort-error
|
||||
(t/testing "AbortError DOMException is ignorable"
|
||||
(let [cause (make-error "AbortError" "The operation was aborted")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-zone-js-tostring
|
||||
(t/testing "Zone.js toString read-only property error is ignorable"
|
||||
(let [cause (make-error "TypeError"
|
||||
"Cannot assign to read only property 'toString' of function 'function () { [native code] }'")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-ignorable-not-found-error-remove-child
|
||||
(t/testing "NotFoundError with removeChild message is ignorable"
|
||||
(let [cause (make-error "NotFoundError"
|
||||
"Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node."
|
||||
:stack "NotFoundError: Failed to execute 'removeChild'\n at zLe (libs.js:1:1)")]
|
||||
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-not-ignorable-not-found-error-other
|
||||
(t/testing "NotFoundError without removeChild is NOT ignorable"
|
||||
(let [cause (make-error "NotFoundError"
|
||||
"Failed to execute 'insertBefore' on 'Node': something else")]
|
||||
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-not-ignorable-regular-error
|
||||
(t/testing "Regular application errors are NOT ignorable"
|
||||
(let [cause (make-error "Error" "Cannot read property 'x' of undefined")]
|
||||
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||
|
||||
(t/deftest test-not-ignorable-type-error
|
||||
(t/testing "Regular TypeError is NOT ignorable"
|
||||
(let [cause (make-error "TypeError" "undefined is not a function")]
|
||||
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||
@ -9,6 +9,7 @@
|
||||
[frontend-tests.data.workspace-media-test]
|
||||
[frontend-tests.data.workspace-texts-test]
|
||||
[frontend-tests.data.workspace-thumbnails-test]
|
||||
[frontend-tests.errors-test]
|
||||
[frontend-tests.helpers-shapes-test]
|
||||
[frontend-tests.logic.comp-remove-swap-slots-test]
|
||||
[frontend-tests.logic.components-and-tokens]
|
||||
@ -44,6 +45,7 @@
|
||||
(t/run-tests
|
||||
'frontend-tests.basic-shapes-test
|
||||
'frontend-tests.data.repo-test
|
||||
'frontend-tests.errors-test
|
||||
'frontend-tests.main-errors-test
|
||||
'frontend-tests.data.uploads-test
|
||||
'frontend-tests.data.viewer-test
|
||||
|
||||
@ -386,6 +386,9 @@ For many tasks, it can be critical to visually inspect the design. Remember to u
|
||||
* When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
|
||||
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
|
||||
non-creative defaults such as white/black if you are lacking information).
|
||||
* When creating new designs,
|
||||
- ensure a clean internal structure by applying flex and grid layouts when appropriate
|
||||
- ensure proper semantic naming of elements.
|
||||
|
||||
# Revising Designs
|
||||
|
||||
|
||||
@ -19,6 +19,6 @@
|
||||
"@github/copilot": "^1.0.21",
|
||||
"@types/node": "^25.5.2",
|
||||
"esbuild": "^0.28.0",
|
||||
"opencode-ai": "^1.4.3"
|
||||
"opencode-ai": "^1.14.19"
|
||||
}
|
||||
}
|
||||
|
||||
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
@ -18,8 +18,8 @@ importers:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0
|
||||
opencode-ai:
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3
|
||||
specifier: ^1.14.19
|
||||
version: 1.14.19
|
||||
|
||||
packages:
|
||||
|
||||
@ -227,67 +227,67 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
opencode-ai@1.4.3:
|
||||
resolution: {integrity: sha512-WwCSrLgJiS+sLIWoi9pa62vAw3l6VI3a+ShhjDDMUJBBG2FxU18xEhk8xhEedLMKyHo1p0nwD41+iKZ1y+rdAw==}
|
||||
opencode-ai@1.14.19:
|
||||
resolution: {integrity: sha512-67h56qYcJivd2U9VK8LJvyMBCc3ZE3HcJ/qL4YtaidSnjEumy4SxO+HHlDISsLase7TUQ0w1nULOAibdZxGzbQ==}
|
||||
hasBin: true
|
||||
|
||||
opencode-darwin-arm64@1.4.3:
|
||||
resolution: {integrity: sha512-d/MT28Is5yhdFY+36AqKc5r31zx8lXTQIYblfn5R8kdhamXijZVGdD0pHl3eJc1ZolUHNwzg2B+IqV22uyU9GQ==}
|
||||
opencode-darwin-arm64@1.14.19:
|
||||
resolution: {integrity: sha512-gjJ97dTiBbCas3Y7K6KQMQm5/KNIBOM9+10KZHqNr2bQfn0N09O977ZkXoX6IVBBE2632Ahaa71d4pzLKN9ULw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
opencode-darwin-x64-baseline@1.4.3:
|
||||
resolution: {integrity: sha512-WTqf7WBNRZcv6pClqnN4F7X/T/osgcPGikNHkHUSLszKWg9flqz7Z68kHR4i9ae8Bn3ke9MQRgzRdOt2PgLL0w==}
|
||||
opencode-darwin-x64-baseline@1.14.19:
|
||||
resolution: {integrity: sha512-UuwLpa511h7qQ+rTmOUmsHch/N4NBQT64dzg+iLWnR5yoR/81inENpbwxiS7hXpVdzCUGo/YnxI1u6SIBoMlTQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
opencode-darwin-x64@1.4.3:
|
||||
resolution: {integrity: sha512-8FUHeybVmaCYt4S2YmWcf32o/xa/ahCfI258bpWssrzs7Xg51JgUB/Csoble0I1mH7RpW39SKy/hHUtHGuJfJg==}
|
||||
opencode-darwin-x64@1.14.19:
|
||||
resolution: {integrity: sha512-uksrjOtWI7Ob5JvjZBSsrKuy3JVF9d89oYZYfWS5m8ordNyv1Nob39MXJXizv85ozsXjSb0rqjpJurnJw8K+tQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
opencode-linux-arm64-musl@1.4.3:
|
||||
resolution: {integrity: sha512-3Ej2klaep8+fxcc44UyEuRpb/UFiNkwfzIDLIST83hFUtjzprjpTRqg6zHmOfzyfjNAaNpB4VZw6e9y3mGBpiQ==}
|
||||
opencode-linux-arm64-musl@1.14.19:
|
||||
resolution: {integrity: sha512-R1BJuBGWHfBxfKvIA/Hb4nhYaJgCKl1B+mAGNydu+z0CLtGtwU8r+kQWF/G2N0y8Vx6Y6DRfJiv1X0eZEfD1BQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
opencode-linux-arm64@1.4.3:
|
||||
resolution: {integrity: sha512-9jpVSOEF7TX3gPPAHVAsBT9XEO3LgYafI+IUmOzbBB9CDiVVNJw6JmEffmSpSxY4nkAh322xnMbNjVGEyXQBRA==}
|
||||
opencode-linux-arm64@1.14.19:
|
||||
resolution: {integrity: sha512-Bo+aZOppLF366mgGfK0CnIcAVy1EmsrBv93eot1CmPSN1oeud07GpGdn3Bjl5f6KuBx1io6JsvjQyHno+MH5AA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
opencode-linux-x64-baseline-musl@1.4.3:
|
||||
resolution: {integrity: sha512-aned/3FQTHXXQv2PPKDprJwQaQkoadriQ6AByGhRl6/bHhSkhkiVl6cHHvYMKxYEwN4bVOydWhasfgm/xru/xw==}
|
||||
opencode-linux-x64-baseline-musl@1.14.19:
|
||||
resolution: {integrity: sha512-+K8MuGoHugtUec4P/nKcTwZFUipHfW7oPpwlIoPiAQou3bNFTzzP6rslbzzNwjXlQRsUw9GAtuIPDOCL6CkgDg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
opencode-linux-x64-baseline@1.4.3:
|
||||
resolution: {integrity: sha512-HpzdgYaI90qqt0WokcyBhadgFQ0EYMhq4TZ4EcaSPuZTssS2Drb6kp70Si54uOJL/MUAdc9+E0BYYIAdOJ6h1g==}
|
||||
opencode-linux-x64-baseline@1.14.19:
|
||||
resolution: {integrity: sha512-KuvITzg4iK0hdIjpNZepwu3bLZ/dUZDI6BwCoV4w//VEP1j3UfDyeS3vWghKcQLd2T1+yybuEMM/3RXcwm/lGQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
opencode-linux-x64-musl@1.4.3:
|
||||
resolution: {integrity: sha512-ibUevyDxVrwkp6FWu8UBCBsrzlKDT/uEug2NHCKaHIwo9uwVf5zsL/0ueHYqmH14SHK+M6wzWewYk6WuW9f0zQ==}
|
||||
opencode-linux-x64-musl@1.14.19:
|
||||
resolution: {integrity: sha512-0qe2+X76UJdrCdhdlJyfubMC4tveHAVxjSmPq7g9Zm95heBeJdcQDCLeyQk/lGgeXgsZzVPfLmyTWNtBvCZYFQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
opencode-linux-x64@1.4.3:
|
||||
resolution: {integrity: sha512-RS6TsDqTUrW5sefxD1KD9Xy9mSYGXAlr2DlGrdi8vNm9e/Bt4r4u557VB7f/Uj2CxTt2Gf7OWl08ZoPlxMJ5Gg==}
|
||||
opencode-linux-x64@1.14.19:
|
||||
resolution: {integrity: sha512-2GljfL7BeG4xALBJVRwaAGCM/dzYF5aQf6bfLTKsQIl6QpLUguYSF+fkStBHLeehyqbDP5MtiEEuXjC0+mecjA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
opencode-windows-arm64@1.4.3:
|
||||
resolution: {integrity: sha512-2ViH17WpIQbRVfQaOBMi49pu73gqTQYT/4/WxFjShmRagX40/KkG18fhvyDAZrBKfkhPtdwgFsFxMSYP9F6QCQ==}
|
||||
opencode-windows-arm64@1.14.19:
|
||||
resolution: {integrity: sha512-8/vRHe5tHexikfPceLmpjsQiEhuDTOSCSlEmP4s0Yq3UAkVaDAxpiWq7Bx4g8hjr1gzfXD9vbjV+WHq/BtMC/w==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
opencode-windows-x64-baseline@1.4.3:
|
||||
resolution: {integrity: sha512-SWYDli9SAKQd/pS/hVfuq1KEsc+gnAJdv+YtBmxaHOw57y0euqLwbGFUYFq78GAMGt/RnTYWZIEUbRK/ZiX3UA==}
|
||||
opencode-windows-x64-baseline@1.14.19:
|
||||
resolution: {integrity: sha512-Z8imEJK/srE/r1fr7oNLvpLTeRJQyuL7vsbXvCt3T7j2Ew9BOZ7RuYa8EE0R6bNqQ+MLhBGPiAG7NWc++MgK8Q==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
opencode-windows-x64@1.4.3:
|
||||
resolution: {integrity: sha512-UxmKDIw3t4XHST6JSUWHmSrCGIEK1LRTAOpO82HBC3XkIjH78gVIeauRR6RULjWAApmy9I1C3TukO2sDUi7Gvw==}
|
||||
opencode-windows-x64@1.14.19:
|
||||
resolution: {integrity: sha512-/TqGN91WiUzx7IPMPwmpMIzRixi5TMjaBcC9FH1TgD7DCqKnP6pokvu+ak0C9xwA4wKorE9PoZzeRt/+c3rDCQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
@ -434,55 +434,55 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.28.0
|
||||
'@esbuild/win32-x64': 0.28.0
|
||||
|
||||
opencode-ai@1.4.3:
|
||||
opencode-ai@1.14.19:
|
||||
optionalDependencies:
|
||||
opencode-darwin-arm64: 1.4.3
|
||||
opencode-darwin-x64: 1.4.3
|
||||
opencode-darwin-x64-baseline: 1.4.3
|
||||
opencode-linux-arm64: 1.4.3
|
||||
opencode-linux-arm64-musl: 1.4.3
|
||||
opencode-linux-x64: 1.4.3
|
||||
opencode-linux-x64-baseline: 1.4.3
|
||||
opencode-linux-x64-baseline-musl: 1.4.3
|
||||
opencode-linux-x64-musl: 1.4.3
|
||||
opencode-windows-arm64: 1.4.3
|
||||
opencode-windows-x64: 1.4.3
|
||||
opencode-windows-x64-baseline: 1.4.3
|
||||
opencode-darwin-arm64: 1.14.19
|
||||
opencode-darwin-x64: 1.14.19
|
||||
opencode-darwin-x64-baseline: 1.14.19
|
||||
opencode-linux-arm64: 1.14.19
|
||||
opencode-linux-arm64-musl: 1.14.19
|
||||
opencode-linux-x64: 1.14.19
|
||||
opencode-linux-x64-baseline: 1.14.19
|
||||
opencode-linux-x64-baseline-musl: 1.14.19
|
||||
opencode-linux-x64-musl: 1.14.19
|
||||
opencode-windows-arm64: 1.14.19
|
||||
opencode-windows-x64: 1.14.19
|
||||
opencode-windows-x64-baseline: 1.14.19
|
||||
|
||||
opencode-darwin-arm64@1.4.3:
|
||||
opencode-darwin-arm64@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-darwin-x64-baseline@1.4.3:
|
||||
opencode-darwin-x64-baseline@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-darwin-x64@1.4.3:
|
||||
opencode-darwin-x64@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-linux-arm64-musl@1.4.3:
|
||||
opencode-linux-arm64-musl@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-linux-arm64@1.4.3:
|
||||
opencode-linux-arm64@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-linux-x64-baseline-musl@1.4.3:
|
||||
opencode-linux-x64-baseline-musl@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-linux-x64-baseline@1.4.3:
|
||||
opencode-linux-x64-baseline@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-linux-x64-musl@1.4.3:
|
||||
opencode-linux-x64-musl@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-linux-x64@1.4.3:
|
||||
opencode-linux-x64@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-windows-arm64@1.4.3:
|
||||
opencode-windows-arm64@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-windows-x64-baseline@1.4.3:
|
||||
opencode-windows-x64-baseline@1.14.19:
|
||||
optional: true
|
||||
|
||||
opencode-windows-x64@1.4.3:
|
||||
opencode-windows-x64@1.14.19:
|
||||
optional: true
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
@ -145,6 +145,48 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_viewport_interest_area_threshold(
|
||||
viewport_interest_area_threshold: i32,
|
||||
) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_viewport_interest_area_threshold(viewport_interest_area_threshold);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_max_blocking_time_ms(max_blocking_time_ms: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_max_blocking_time_ms(max_blocking_time_ms);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_node_batch_threshold(node_batch_threshold: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_node_batch_threshold(node_batch_threshold);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_blur_downscale_threshold(blur_downscale_threshold: f32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_blur_downscale_threshold(blur_downscale_threshold);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_antialias_threshold(threshold: f32) -> Result<()> {
|
||||
@ -401,6 +443,42 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enter interactive transform mode (drag / resize / rotate of a
|
||||
/// shape). Activates the same expensive-effect skipping as pan/zoom
|
||||
/// (`fast_mode`) but keeps per-frame flushing enabled so the Target is
|
||||
/// presented every rAF, and triggers atlas-backed backdrops so
|
||||
/// invalidated tiles do not appear sequentially or flicker.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_modifiers_start");
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(true);
|
||||
opts.set_interactive_transform(true);
|
||||
performance::end_measure!("set_modifiers_start");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leave interactive transform mode and cancel any pending async
|
||||
/// render scheduled under it. The caller is responsible for triggering
|
||||
/// a final full-quality render (typically via `_render`) once the
|
||||
/// modifiers have been committed.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_modifiers_end");
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(false);
|
||||
opts.set_interactive_transform(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clear_focus_mode() -> Result<()> {
|
||||
@ -939,6 +1017,10 @@ pub extern "C" fn render_shape_pixels(
|
||||
) -> Result<*mut u8> {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
|
||||
if !scale.is_finite() {
|
||||
return Err(Error::CriticalError("Scale is not finite".to_string()));
|
||||
}
|
||||
|
||||
with_state_mut!(state, {
|
||||
let (data, width, height) =
|
||||
state.render_shape_pixels(&id, scale, performance::get_time())?;
|
||||
|
||||
@ -37,15 +37,6 @@ use crate::wapi;
|
||||
pub use fonts::*;
|
||||
pub use images::*;
|
||||
|
||||
// This is the extra area used for tile rendering (tiles beyond viewport).
|
||||
// Higher values pre-render more tiles, reducing empty squares during pan but using more memory.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -345,9 +336,8 @@ pub(crate) struct RenderState {
|
||||
pub cache_cleared_this_render: bool,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let interest = VIEWPORT_INTEREST_AREA_THRESHOLD;
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(viewbox, interest, scale);
|
||||
|
||||
@ -382,10 +372,11 @@ impl RenderState {
|
||||
|
||||
let viewbox = Viewbox::new(width as f32, height as f32);
|
||||
let tiles = tiles::TileHashMap::new();
|
||||
let options = RenderOptions::default();
|
||||
|
||||
Ok(RenderState {
|
||||
gpu_state: gpu_state.clone(),
|
||||
options: RenderOptions::default(),
|
||||
options,
|
||||
surfaces,
|
||||
fonts,
|
||||
viewbox,
|
||||
@ -402,7 +393,7 @@ impl RenderState {
|
||||
tiles,
|
||||
tile_viewbox: tiles::TileViewbox::new_with_interest(
|
||||
viewbox,
|
||||
VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
options.viewport_interest_area_threshold,
|
||||
1.0,
|
||||
),
|
||||
pending_tiles: PendingTiles::new_empty(),
|
||||
@ -631,6 +622,22 @@ impl RenderState {
|
||||
self.options.set_antialias_threshold(value);
|
||||
}
|
||||
|
||||
pub fn set_viewport_interest_area_threshold(&mut self, value: i32) {
|
||||
self.options.set_viewport_interest_area_threshold(value);
|
||||
}
|
||||
|
||||
pub fn set_node_batch_threshold(&mut self, value: i32) {
|
||||
self.options.set_node_batch_threshold(value);
|
||||
}
|
||||
|
||||
pub fn set_max_blocking_time_ms(&mut self, value: i32) {
|
||||
self.options.set_max_blocking_time_ms(value);
|
||||
}
|
||||
|
||||
pub fn set_blur_downscale_threshold(&mut self, value: f32) {
|
||||
self.options.set_blur_downscale_threshold(value);
|
||||
}
|
||||
|
||||
pub fn set_background_color(&mut self, color: skia::Color) {
|
||||
self.background_color = color;
|
||||
}
|
||||
@ -1495,7 +1502,7 @@ impl RenderState {
|
||||
// Scale and translate the target according to the cached data
|
||||
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
|
||||
|
||||
let interest = VIEWPORT_INTEREST_AREA_THRESHOLD;
|
||||
let interest = self.options.viewport_interest_area_threshold;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(
|
||||
self.cached_viewbox,
|
||||
@ -1666,6 +1673,24 @@ impl RenderState {
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
self.reset_canvas();
|
||||
|
||||
// During an interactive shape transform (drag/resize/rotate) the
|
||||
// Target is repainted tile-by-tile. If only a subset of the
|
||||
// invalidated tiles finishes in this rAF the remaining area
|
||||
// would either show stale content from the previous frame or,
|
||||
// on buffer swaps, show blank pixels — either way the user
|
||||
// perceives tiles appearing sequentially. Paint the persistent
|
||||
// 1:1 atlas as a stable backdrop so every flush presents a
|
||||
// coherent picture: unchanged tiles come from the atlas and
|
||||
// invalidated tiles are overwritten on top as they finish.
|
||||
if self.options.is_interactive_transform() && self.surfaces.has_atlas() {
|
||||
self.surfaces.draw_atlas_to_target(
|
||||
self.viewbox,
|
||||
self.options.dpr(),
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
@ -1674,15 +1699,25 @@ impl RenderState {
|
||||
s.canvas().scale((scale, scale));
|
||||
});
|
||||
|
||||
let viewbox_cache_size = get_cache_size(self.viewbox, scale);
|
||||
let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, scale);
|
||||
let viewbox_cache_size = get_cache_size(
|
||||
self.viewbox,
|
||||
scale,
|
||||
self.options.viewport_interest_area_threshold,
|
||||
);
|
||||
let cached_viewbox_cache_size = get_cache_size(
|
||||
self.cached_viewbox,
|
||||
scale,
|
||||
self.options.viewport_interest_area_threshold,
|
||||
);
|
||||
// Only resize cache if the new size is larger than the cached size
|
||||
// This avoids unnecessary surface recreations when the cache size decreases
|
||||
if viewbox_cache_size.width > cached_viewbox_cache_size.width
|
||||
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
|
||||
{
|
||||
self.surfaces
|
||||
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD)?;
|
||||
self.surfaces.resize_cache(
|
||||
viewbox_cache_size,
|
||||
self.options.viewport_interest_area_threshold,
|
||||
)?;
|
||||
}
|
||||
|
||||
// FIXME - review debug
|
||||
@ -1744,12 +1779,16 @@ impl RenderState {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
}
|
||||
|
||||
// In fast mode (pan/zoom in progress), render_from_cache owns
|
||||
// the Target surface — skip flush so we don't present stale
|
||||
// tile positions. The rAF still populates the Cache surface
|
||||
// and tile HashMap so render_from_cache progressively shows
|
||||
// more complete content.
|
||||
if !self.options.is_fast_mode() {
|
||||
// In a pure viewport interaction (pan/zoom), render_from_cache
|
||||
// owns the Target surface — skip flush so we don't present
|
||||
// stale tile positions. The rAF still populates the Cache
|
||||
// surface and tile HashMap so render_from_cache progressively
|
||||
// shows more complete content.
|
||||
//
|
||||
// During interactive shape transforms (drag/resize/rotate) we
|
||||
// still need to flush every rAF so the user sees the updated
|
||||
// shape position — render_from_cache is not in the loop here.
|
||||
if !self.options.is_viewport_interaction() {
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
@ -1887,8 +1926,26 @@ impl RenderState {
|
||||
|
||||
#[inline]
|
||||
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
|
||||
iteration % NODE_BATCH_THRESHOLD == 0
|
||||
&& performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS
|
||||
if iteration % self.options.node_batch_threshold != 0 {
|
||||
return false;
|
||||
}
|
||||
if performance::get_time() - timestamp <= self.options.max_blocking_time_ms {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During interactive shape transforms we must complete every
|
||||
// visible tile in a single rAF so the user never sees tiles
|
||||
// popping in sequentially. Only yield once all visible work is
|
||||
// done and we are processing the interest-area pre-render.
|
||||
if self.options.is_interactive_transform() {
|
||||
if let Some(tile) = self.current_tile {
|
||||
if self.tile_viewbox.is_visible(&tile) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -2263,9 +2320,10 @@ impl RenderState {
|
||||
// Bounds above were computed from the original sigma so filter surface coverage is correct.
|
||||
// Maximum downscale is 1/BLUR_DOWNSCALE_THRESHOLD (i.e. 8x): beyond that the
|
||||
// filter surface becomes too small and quality degrades noticeably.
|
||||
const MIN_BLUR_DOWNSCALE: f32 = 1.0 / BLUR_DOWNSCALE_THRESHOLD;
|
||||
let blur_downscale = if shadow.blur > BLUR_DOWNSCALE_THRESHOLD {
|
||||
(BLUR_DOWNSCALE_THRESHOLD / shadow.blur).max(MIN_BLUR_DOWNSCALE)
|
||||
let blur_downscale_threshold: f32 = self.options.blur_downscale_threshold;
|
||||
let min_blur_downscale: f32 = 1.0 / blur_downscale_threshold;
|
||||
let blur_downscale = if shadow.blur > blur_downscale_threshold {
|
||||
(blur_downscale_threshold / shadow.blur).max(min_blur_downscale)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::math::Rect as MathRect;
|
||||
use crate::shapes::ImageFill;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::error::Result;
|
||||
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
|
||||
use skia_safe::{self as skia, Codec, ISize};
|
||||
use std::collections::HashMap;
|
||||
@ -159,7 +159,7 @@ impl ImageStore {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
return Err(Error::RecoverableError("Image already exists".to_string()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let raw_data = image_data.to_vec();
|
||||
@ -186,7 +186,7 @@ impl ImageStore {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
return Err(Error::RecoverableError("Image already exists".to_string()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a Skia image from the existing GL texture
|
||||
|
||||
@ -4,13 +4,30 @@ const PROFILE_REBUILD_TILES: u32 = 0x02;
|
||||
const TEXT_EDITOR_V3: u32 = 0x04;
|
||||
const SHOW_WASM_INFO: u32 = 0x08;
|
||||
|
||||
// Render performance options
|
||||
// This is the extra area used for tile rendering (tiles beyond viewport).
|
||||
// Higher values pre-render more tiles, reducing empty squares during pan but using more memory.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
const ANTIALIAS_THRESHOLD: f32 = 7.0;
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct RenderOptions {
|
||||
pub flags: u32,
|
||||
pub dpr: Option<f32>,
|
||||
fast_mode: bool,
|
||||
/// Active while the user is interacting with a shape (drag, resize,
|
||||
/// rotate). Implies `fast_mode` semantics for expensive effects but
|
||||
/// keeps per-frame flushing enabled (unlike pan/zoom, where
|
||||
/// `render_from_cache` drives target presentation).
|
||||
interactive_transform: bool,
|
||||
/// Minimum on-screen size (CSS px at 1:1 zoom) above which vector antialiasing is enabled.
|
||||
pub antialias_threshold: f32,
|
||||
pub viewport_interest_area_threshold: i32,
|
||||
pub max_blocking_time_ms: i32,
|
||||
pub node_batch_threshold: i32,
|
||||
pub blur_downscale_threshold: f32,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
@ -19,7 +36,12 @@ impl Default for RenderOptions {
|
||||
flags: 0,
|
||||
dpr: None,
|
||||
fast_mode: false,
|
||||
antialias_threshold: 7.0,
|
||||
interactive_transform: false,
|
||||
antialias_threshold: ANTIALIAS_THRESHOLD,
|
||||
viewport_interest_area_threshold: VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
max_blocking_time_ms: MAX_BLOCKING_TIME_MS,
|
||||
node_batch_threshold: NODE_BATCH_THRESHOLD,
|
||||
blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,6 +64,26 @@ impl RenderOptions {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
/// Interactive transform is ON while the user is dragging, resizing
|
||||
/// or rotating a shape. Callers use it to keep per-frame flushing
|
||||
/// enabled and to render visible tiles in a single frame so tiles
|
||||
/// never appear sequentially or flicker during the gesture.
|
||||
pub fn is_interactive_transform(&self) -> bool {
|
||||
self.interactive_transform
|
||||
}
|
||||
|
||||
pub fn set_interactive_transform(&mut self, enabled: bool) {
|
||||
self.interactive_transform = enabled;
|
||||
}
|
||||
|
||||
/// True only when the viewport is the one being moved (pan/zoom)
|
||||
/// and the dedicated `render_from_cache` path owns Target
|
||||
/// presentation. In this mode `process_animation_frame` must not
|
||||
/// flush to avoid presenting stale tile positions.
|
||||
pub fn is_viewport_interaction(&self) -> bool {
|
||||
self.fast_mode && !self.interactive_transform
|
||||
}
|
||||
|
||||
pub fn dpr(&self) -> f32 {
|
||||
self.dpr.unwrap_or(1.0)
|
||||
}
|
||||
@ -59,4 +101,28 @@ impl RenderOptions {
|
||||
self.antialias_threshold = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_blur_downscale_threshold(&mut self, value: f32) {
|
||||
if value.is_finite() && value > 0.0 {
|
||||
self.blur_downscale_threshold = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_viewport_interest_area_threshold(&mut self, value: i32) {
|
||||
if value >= 0 {
|
||||
self.viewport_interest_area_threshold = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_node_batch_threshold(&mut self, value: i32) {
|
||||
if value > 0 {
|
||||
self.node_batch_threshold = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_max_blocking_time_ms(&mut self, value: i32) {
|
||||
if value > 0 {
|
||||
self.max_blocking_time_ms = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -938,36 +938,49 @@ impl Surfaces {
|
||||
|
||||
if max_w > self.extra_tile_dims.width || max_h > self.extra_tile_dims.height {
|
||||
self.extra_tile_dims = skia::ISize::new(max_w, max_h);
|
||||
self.drop_shadows = self
|
||||
|
||||
if let Some(surface) = self
|
||||
.drop_shadows
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
.unwrap();
|
||||
self.inner_shadows = self
|
||||
{
|
||||
self.drop_shadows = surface;
|
||||
}
|
||||
|
||||
if let Some(surface) = self
|
||||
.inner_shadows
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
.unwrap();
|
||||
self.text_drop_shadows = self
|
||||
{
|
||||
self.inner_shadows = surface;
|
||||
}
|
||||
|
||||
if let Some(surface) = self
|
||||
.text_drop_shadows
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
.unwrap();
|
||||
self.text_drop_shadows = self
|
||||
.text_drop_shadows
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
.unwrap();
|
||||
self.shape_strokes = self
|
||||
{
|
||||
self.text_drop_shadows = surface;
|
||||
}
|
||||
|
||||
if let Some(surface) = self
|
||||
.shape_strokes
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
.unwrap();
|
||||
self.shape_fills = self
|
||||
{
|
||||
self.shape_strokes = surface;
|
||||
}
|
||||
|
||||
if let Some(surface) = self
|
||||
.shape_strokes
|
||||
.new_surface_with_dimensions((max_w, max_h))
|
||||
.unwrap();
|
||||
{
|
||||
self.shape_fills = surface;
|
||||
}
|
||||
}
|
||||
|
||||
self.export = self
|
||||
if let Some(surface) = self
|
||||
.export
|
||||
.new_surface_with_dimensions((target_w, target_h))
|
||||
.unwrap();
|
||||
{
|
||||
self.export = surface;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
use crate::render::options::RenderOptions;
|
||||
use crate::shapes::{Shape, TextContent, Type, VerticalAlign};
|
||||
use crate::state::{TextEditorState, TextSelection};
|
||||
use crate::view::Viewbox;
|
||||
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||
use skia_safe::{BlendMode, Canvas, Paint, Rect};
|
||||
|
||||
pub fn render_overlay(canvas: &Canvas, editor_state: &TextEditorState, shape: &Shape) {
|
||||
pub fn render_overlay(
|
||||
canvas: &Canvas,
|
||||
viewbox: &Viewbox,
|
||||
options: &RenderOptions,
|
||||
editor_state: &TextEditorState,
|
||||
shape: &Shape,
|
||||
) {
|
||||
if !editor_state.has_focus {
|
||||
return;
|
||||
}
|
||||
@ -13,17 +21,24 @@ pub fn render_overlay(canvas: &Canvas, editor_state: &TextEditorState, shape: &S
|
||||
};
|
||||
|
||||
canvas.save();
|
||||
let zoom = viewbox.zoom * options.dpr();
|
||||
canvas.scale((zoom, zoom));
|
||||
canvas.translate((-viewbox.area.left, -viewbox.area.top));
|
||||
|
||||
if editor_state.selection.is_selection() {
|
||||
render_selection(canvas, editor_state, text_content, shape);
|
||||
}
|
||||
|
||||
if editor_state.cursor_visible {
|
||||
render_cursor(canvas, editor_state, text_content, shape);
|
||||
render_cursor(canvas, zoom, editor_state, text_content, shape);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn render_cursor(
|
||||
canvas: &Canvas,
|
||||
zoom: f32,
|
||||
editor_state: &TextEditorState,
|
||||
text_content: &TextContent,
|
||||
shape: &Shape,
|
||||
@ -32,6 +47,9 @@ fn render_cursor(
|
||||
return;
|
||||
};
|
||||
|
||||
let mut cursor_rect = Rect::new_empty();
|
||||
cursor_rect.set_xywh(rect.x(), rect.y(), 1.5 / zoom, rect.height());
|
||||
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color(editor_state.theme.cursor_color);
|
||||
paint.set_anti_alias(true);
|
||||
@ -39,7 +57,7 @@ fn render_cursor(
|
||||
let shape_matrix = shape.get_matrix();
|
||||
canvas.save();
|
||||
canvas.concat(&shape_matrix);
|
||||
canvas.draw_rect(rect, &paint);
|
||||
canvas.draw_rect(cursor_rect, &paint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@ -160,7 +178,7 @@ fn calculate_cursor_rect(
|
||||
return Some(Rect::from_xywh(
|
||||
cursor_x,
|
||||
y_offset + cursor_y,
|
||||
editor_state.theme.cursor_width,
|
||||
1.0, // cursor_width
|
||||
cursor_height,
|
||||
));
|
||||
}
|
||||
|
||||
@ -100,7 +100,6 @@ pub enum TextEditorEvent {
|
||||
|
||||
/// FIXME: It should be better to get these constants from the frontend through the API.
|
||||
const SELECTION_COLOR: Color = Color::from_argb(127, 0, 209, 184);
|
||||
const CURSOR_WIDTH: f32 = 1.5;
|
||||
const CURSOR_COLOR: Color = Color::BLACK;
|
||||
const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0;
|
||||
|
||||
@ -257,7 +256,6 @@ impl TextEditorStyles {
|
||||
|
||||
pub struct TextEditorTheme {
|
||||
pub selection_color: Color,
|
||||
pub cursor_width: f32,
|
||||
pub cursor_color: Color,
|
||||
}
|
||||
|
||||
@ -340,7 +338,6 @@ impl TextEditorState {
|
||||
Self {
|
||||
theme: TextEditorTheme {
|
||||
selection_color: SELECTION_COLOR,
|
||||
cursor_width: CURSOR_WIDTH,
|
||||
cursor_color: CURSOR_COLOR,
|
||||
},
|
||||
selection: TextSelection::new(),
|
||||
|
||||
@ -314,9 +314,9 @@ pub extern "C" fn set_shape_grow_type(grow_type: u8) {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
if let Type::Text(text_content) = &mut shape.shape_type {
|
||||
text_content.set_grow_type(GrowType::from(grow_type));
|
||||
} else {
|
||||
panic!("Trying to update grow type in a shape that it's not a text shape");
|
||||
}
|
||||
// Don't throw error if the object is not text.
|
||||
// On swap component opperations is convenient.
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -32,16 +32,11 @@ pub enum CursorDirection {
|
||||
// ============================================================================
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_apply_theme(
|
||||
selection_color: u32,
|
||||
cursor_width: f32,
|
||||
cursor_color: u32,
|
||||
) {
|
||||
pub extern "C" fn text_editor_apply_theme(selection_color: u32, cursor_color: u32) {
|
||||
with_state_mut!(state, {
|
||||
// NOTE: In the future could be interesting to fill al this data from
|
||||
// a structure pointer.
|
||||
state.text_editor_state.theme.selection_color = Color::new(selection_color);
|
||||
state.text_editor_state.theme.cursor_width = cursor_width;
|
||||
state.text_editor_state.theme.cursor_color = Color::new(cursor_color);
|
||||
})
|
||||
}
|
||||
@ -912,13 +907,14 @@ pub extern "C" fn text_editor_render_overlay() {
|
||||
};
|
||||
|
||||
let canvas = state.render_state.surfaces.canvas(SurfaceId::Target);
|
||||
canvas.save();
|
||||
let viewbox = state.render_state.viewbox;
|
||||
let zoom = viewbox.zoom * state.render_state.options.dpr();
|
||||
canvas.scale((zoom, zoom));
|
||||
canvas.translate((-viewbox.area.left, -viewbox.area.top));
|
||||
text_editor_render::render_overlay(canvas, &state.text_editor_state, shape);
|
||||
canvas.restore();
|
||||
text_editor_render::render_overlay(
|
||||
canvas,
|
||||
&viewbox,
|
||||
&state.render_state.options,
|
||||
&state.text_editor_state,
|
||||
shape,
|
||||
);
|
||||
state.render_state.flush_and_submit();
|
||||
});
|
||||
}
|
||||
@ -1103,12 +1099,11 @@ fn get_cursor_rect(
|
||||
(pos.position as f32, height)
|
||||
};
|
||||
|
||||
let cursor_width = 2.0;
|
||||
let selrect = shape.selrect();
|
||||
let base_x = selrect.x();
|
||||
let base_y = selrect.y() + y_offset;
|
||||
|
||||
return Some(Rect::from_xywh(base_x + x, base_y, cursor_width, height));
|
||||
return Some(Rect::from_xywh(base_x + x, base_y, 1.0, height));
|
||||
}
|
||||
y_offset += laid_out_para.height();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user