diff --git a/frontend/resources/images/icons/code.svg b/frontend/resources/images/icons/code.svg new file mode 100644 index 000000000..d8ae15c9e --- /dev/null +++ b/frontend/resources/images/icons/code.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/copy.svg b/frontend/resources/images/icons/copy.svg index 41863f61e..8a65c91c7 100644 --- a/frontend/resources/images/icons/copy.svg +++ b/frontend/resources/images/icons/copy.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/resources/images/icons/interaction.svg b/frontend/resources/images/icons/interaction.svg index 0fd5dd946..217110b57 100644 --- a/frontend/resources/images/icons/interaction.svg +++ b/frontend/resources/images/icons/interaction.svg @@ -1 +1,5 @@ - + + + + + diff --git a/frontend/resources/images/icons/picker.svg b/frontend/resources/images/icons/picker.svg index 3fc711f38..99c8edc76 100644 --- a/frontend/resources/images/icons/picker.svg +++ b/frontend/resources/images/icons/picker.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/recent.svg b/frontend/resources/images/icons/recent.svg index d9d237fee..590de6517 100644 --- a/frontend/resources/images/icons/recent.svg +++ b/frontend/resources/images/icons/recent.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/resources/images/icons/size-horiz.svg b/frontend/resources/images/icons/size-horiz.svg index 95c19749c..bda1b64e9 100644 --- a/frontend/resources/images/icons/size-horiz.svg +++ b/frontend/resources/images/icons/size-horiz.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/images/icons/size-vert.svg b/frontend/resources/images/icons/size-vert.svg index 7cb95722f..a927a3fd7 100644 --- a/frontend/resources/images/icons/size-vert.svg +++ b/frontend/resources/images/icons/size-vert.svg @@ -1 +1,3 @@ - + + + diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index e2a95f07b..a438f6464 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -29,6 +29,7 @@ @import 'main/layouts/main-layout'; @import "main/layouts/not-found"; @import "main/layouts/viewer"; +@import "main/layouts/handoff"; //################################################# // Commons @@ -80,3 +81,4 @@ @import 'main/partials/workspace-header'; @import 'main/partials/workspace-comments'; @import 'main/partials/color-bullet'; +@import "main/partials/handoff"; diff --git a/frontend/resources/styles/main/layouts/handoff.scss b/frontend/resources/styles/main/layouts/handoff.scss new file mode 100644 index 000000000..2eb5c2548 --- /dev/null +++ b/frontend/resources/styles/main/layouts/handoff.scss @@ -0,0 +1,33 @@ +.handoff-layout { + display: grid; + grid-template-rows: 40px auto; + grid-template-columns: 1fr; + user-select: none; + + &.fullscreen { + .viewer-header { + opacity: 0; + &:hover { + opacity: 1; + } + } + + .viewer-content { + grid-row: 1 / span 2; + } + } + + .viewer-header { + grid-column: 1 / span 1; + grid-row: 1 / span 1; + } + + .viewer-content { + grid-column: 1 / span 1; + grid-row: 2 / span 1; + } +} + +.handoff-layout .settings-bar.settings-bar-left { + left: 0; +} diff --git a/frontend/resources/styles/main/partials/handoff.scss b/frontend/resources/styles/main/partials/handoff.scss new file mode 100644 index 000000000..5e92c3ca3 --- /dev/null +++ b/frontend/resources/styles/main/partials/handoff.scss @@ -0,0 +1,16 @@ +// 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/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +// Copyright (c) 2020 UXBOX Labs SL + +.handoff-svg-wrapper { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 2dad4a320..bc3e60c6d 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -115,6 +115,25 @@ $width-settings-bar: 16rem; } } + .tool-window-bar-icon { + height: 15px; + + svg { + width: 15px; + height: 15px; + } + } + + &.big { + height: 3rem; + padding-bottom: 1rem; + } + + .tool-window-bar-title { + font-size: $fs14; + margin-left: 0.5rem; + } + .tool-window-icon { margin-right: $small; display: none; diff --git a/frontend/resources/styles/main/partials/viewer-header.scss b/frontend/resources/styles/main/partials/viewer-header.scss index 4ee97975e..a123a7471 100644 --- a/frontend/resources/styles/main/partials/viewer-header.scss +++ b/frontend/resources/styles/main/partials/viewer-header.scss @@ -4,7 +4,7 @@ border-bottom: 1px solid $color-gray-60; display: flex; height: 40px; - padding: $x-small $medium $x-small 55px; + padding: 0 $medium 0 55px; position: relative; z-index: 12; justify-content: space-between; @@ -109,6 +109,36 @@ } } + .mode-zone { + display: flex; + height: 100%; + + .mode-zone-button { + background: inherit; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + height: 100%; + width: 40px; + + svg { + fill: $color-gray-20; + width: 24px; + height: 24px; + } + + &.active { + background: $color-gray-60; + svg { + fill: $color-primary; + } + } + } + } + .options-zone { align-items: center; display: flex; diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index 7fc1a5f46..c98b1d1b9 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -51,7 +51,11 @@ :page-id page-id :file-id file-id :interactions-mode :hide - :show-interactions? false})) + :show-interactions? false + + :selected #{} + :collapsed #{} + :hover #{}})) ptk/WatchEvent (watch [_ state stream] @@ -170,24 +174,25 @@ ptk/WatchEvent (watch [_ state stream] (let [route (:route state) + screen (-> route :data :name keyword) qparams (get-in route [:params :query]) pparams (get-in route [:params :path]) index (d/parse-integer (:index qparams))] (when (pos? index) - (rx/of (rt/nav :viewer pparams (assoc qparams :index (dec index))))))))) + (rx/of (rt/nav screen pparams (assoc qparams :index (dec index))))))))) (def select-next-frame (ptk/reify ::select-prev-frame ptk/WatchEvent (watch [_ state stream] (let [route (:route state) + screen (-> route :data :name keyword) qparams (get-in route [:params :query]) pparams (get-in route [:params :path]) index (d/parse-integer (:index qparams)) - total (count (get-in state [:viewer-data :frames]))] (when (< index (dec total)) - (rx/of (rt/nav :viewer pparams (assoc qparams :index (inc index))))))))) + (rx/of (rt/nav screen pparams (assoc qparams :index (inc index))))))))) (defn set-interactions-mode [mode] @@ -249,3 +254,36 @@ "shift+2" #(st/emit! zoom-to-200) "left" #(st/emit! select-prev-frame) "right" #(st/emit! select-next-frame)}) + + +(defn deselect-all [] + (ptk/reify ::deselect-all + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:viewer-local :selected] #{})))) + +(defn select-shape + ([id] (select-shape id false)) + ([id toggle?] + (ptk/reify ::select-shape + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:viewer-local :selected] #{id})))))) + +;; TODO +(defn collapse-all [] + (ptk/reify ::collapse-all)) + +(defn toggle-collapse [id] + (ptk/reify ::toggle-collapse + ptk/UpdateEvent + (update [_ state] + (let [toggled? (contains? (get-in state [:viewer-local :collapsed]) id)] + (update-in state [:viewer-local :collapsed] (if toggled? disj conj) id))))) + +(defn hover-shape [id hover?] + (ptk/reify ::hover-shape + ptk/UpdateEvent + (update [_ state] + (update-in state [:viewer-local :hover] (if hover? conj disj) id)))) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 00be48306..66fddcf8e 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -27,6 +27,7 @@ [app.main.ui.settings :as settings] [app.main.ui.static :refer [not-found-page not-authorized-page]] [app.main.ui.viewer :refer [viewer-page]] + [app.main.ui.viewer.handoff :refer [handoff]] [app.main.ui.workspace :as workspace] [app.util.i18n :as i18n :refer [tr t]] [app.util.timers :as ts] @@ -53,6 +54,7 @@ ["/options" :settings-options]] ["/view/:file-id/:page-id" :viewer] + ["/handoff/:file-id/:page-id" :handoff] ["/not-found" :not-found] ["/not-authorized" :not-authorized] @@ -127,6 +129,14 @@ :index index :token token}]) + :handoff + (let [index (d/parse-integer (get-in route [:params :query :index])) + file-id (uuid (get-in route [:params :path :file-id])) + page-id (uuid (get-in route [:params :path :page-id]))] + [:& handoff {:page-id page-id + :file-id file-id + :index index}]) + :render-object (do (let [file-id (uuid (get-in route [:params :path :file-id])) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index f0347eea1..6410c0478 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -17,7 +17,7 @@ (def align-middle (icon-xref :align-middle)) (def align-top (icon-xref :align-top)) (def alignment (icon-xref :alignment)) -(def arrow (icon-xref :arrow)) +#_(def arrow (icon-xref :arrow)) (def arrow-down (icon-xref :arrow-down)) (def arrow-end (icon-xref :arrow-end)) (def arrow-slide (icon-xref :arrow-slide)) @@ -91,7 +91,7 @@ (def rotate (icon-xref :rotate)) (def ruler (icon-xref :ruler)) (def ruler-tool (icon-xref :ruler-tool)) -(def save (icon-xref :save)) +#_(def save (icon-xref :save)) (def search (icon-xref :search)) (def shape-halign-center (icon-xref :shape-halign-center)) (def shape-halign-left (icon-xref :shape-halign-left)) @@ -128,6 +128,7 @@ (def picker-ramp (icon-xref :picker-ramp)) (def checkbox-checked (icon-xref :checkbox-checked)) (def checkbox-unchecked (icon-xref :checkbox-unchecked)) +(def code (icon-xref :code)) (def loader-pencil (mf/html @@ -149,9 +150,10 @@ (mf/defc debug-icons-preview {::mf/wrap-props false} [props] - [:section.debug-icons-preview + [:section.debug-icons-preview {:style {:background-color "black"}} (for [[key val] (sort-by first (ns-publics 'app.main.ui.icons))] (when (not= key 'debug-icons-preview) - [:div.icon-item {:key key} + [:div.icon-item {:key key + :style {:fill "white"}} (deref val) [:span (pr-str key)]]))]) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index aec718221..21774edd9 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -92,10 +92,12 @@ :toggle-fullscreen toggle-fullscreen :fullscreen? fullscreen? :local local - :index index}] + :index index + :screen :viewer}] [:div.viewer-content {:on-click on-click} (when (:show-thumbnails local) - [:& thumbnails-panel {:index index + [:& thumbnails-panel {:screen :viewer + :index index :data data}]) [:& main-panel {:data data :local local diff --git a/frontend/src/app/main/ui/viewer/handoff.cljs b/frontend/src/app/main/ui/viewer/handoff.cljs new file mode 100644 index 000000000..70371fa4b --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff.cljs @@ -0,0 +1,121 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff + (:require + [rumext.alpha :as mf] + [beicon.core :as rx] + [goog.events :as events] + [okulary.core :as l] + [app.common.exceptions :as ex] + [app.util.data :refer [classnames]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t tr]] + [app.main.data.viewer :as dv] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.hooks :as hooks] + [app.main.ui.icons :as i] + [app.main.ui.keyboard :as kbd] + [app.main.ui.viewer.header :refer [header]] + [app.main.ui.viewer.thumbnails :refer [thumbnails-panel]] + + [app.main.ui.viewer.handoff.render :refer [render-frame-svg]] + [app.main.ui.viewer.handoff.layers-sidebar :refer [layers-sidebar]] + [app.main.ui.viewer.handoff.attributes-sidebar :refer [attributes-sidebar]]) + (:import goog.events.EventType)) + +(defn handle-select-frame [frame] + #(do (dom/prevent-default %) + (dom/stop-propagation %) + (st/emit! (dv/select-shape (:id frame))))) + +(mf/defc render-panel + [{:keys [data local index]}] + (let [locale (mf/deref i18n/locale) + frames (:frames data []) + objects (:objects data) + frame (get frames index)] + [:section.viewer-preview + (cond + (empty? frames) + [:section.empty-state + [:span (t locale "viewer.empty-state")]] + + (nil? frame) + [:section.empty-state + [:span (t locale "viewer.frame-not-found")]] + + :else + [:* + [:& layers-sidebar {:frame frame}] + [:div.handoff-svg-wrapper {:on-click (handle-select-frame frame)} + [:& render-frame-svg {:frame-id (:id frame) + :zoom (:zoom local) + :objects objects}]] + [:& attributes-sidebar]])])) + +(mf/defc handoff-content + [{:keys [data local index] :as props}] + (let [container (mf/use-ref) + + [toggle-fullscreen fullscreen?] (hooks/use-fullscreen container) + + on-mouse-wheel + (fn [event] + (when (kbd/ctrl? event) + (dom/prevent-default event) + (let [event (.getBrowserEvent ^js event)] + (if (pos? (.-deltaY ^js event)) + (st/emit! dv/decrease-zoom) + (st/emit! dv/increase-zoom))))) + + on-mount + (fn [] + ;; bind with passive=false to allow the event to be cancelled + ;; https://stackoverflow.com/a/57582286/3219895 + (let [key1 (events/listen goog/global EventType.WHEEL + on-mouse-wheel #js {"passive" false})] + (fn [] + (events/unlistenByKey key1))))] + + (mf/use-effect on-mount) + (hooks/use-shortcuts dv/shortcuts) + + [:div.handoff-layout {:class (classnames :fullscreen fullscreen?) + :ref container} + [:& header {:data data + :toggle-fullscreen toggle-fullscreen + :fullscreen? fullscreen? + :local local + :index index + :screen :handoff}] + [:div.viewer-content + (when (:show-thumbnails local) + [:& thumbnails-panel {:index index + :data data + :screen :handoff}]) + [:& render-panel {:data data + :local local + :index index}]]])) + +(mf/defc handoff + [{:keys [file-id page-id index] :as props}] + (mf/use-effect + (mf/deps file-id page-id) + (fn [] + (st/emit! (dv/initialize props)))) + + (let [data (mf/deref refs/viewer-data) + local (mf/deref refs/viewer-local)] + (when data + [:& handoff-content {:index index + :local local + :data data}]))) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes_sidebar.cljs new file mode 100644 index 000000000..dd93c613a --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/attributes_sidebar.cljs @@ -0,0 +1,38 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.attributes-sidebar + (:require + [rumext.alpha :as mf] + [app.main.ui.icons :as i] + [app.main.ui.components.tab-container :refer [tab-container tab-element]])) + +(mf/defc info-panel [] + [:div.element-options]) + +(mf/defc code-panel [] + [:div.element-options]) + +(mf/defc attributes-sidebar [] + (let [section (mf/use-state :info #_:code)] + [:aside.settings-bar.settings-bar-right + [:div.settings-bar-inside + [:div.tool-window + [:div.tool-window-bar.big + [:span.tool-window-bar-icon i/text] + [:span.tool-window-bar-title "Text"]] + [:div.tool-window-content + [:& tab-container {:on-change-tab #(reset! section %) + :selected @section} + [:& tab-element {:id :info :title "Info"} + [:& info-panel]] + + [:& tab-element {:id :code :title "Code"} + [:& code-panel]]]]]]])) + diff --git a/frontend/src/app/main/ui/viewer/handoff/layers_sidebar.cljs b/frontend/src/app/main/ui/viewer/handoff/layers_sidebar.cljs new file mode 100644 index 000000000..7e7d269e4 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/layers_sidebar.cljs @@ -0,0 +1,121 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.layers-sidebar + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + [app.common.data :as d] + [app.common.uuid :as uuid] + [app.main.store :as st] + [app.util.dom :as dom] + [app.main.data.viewer :as dv] + [app.main.ui.icons :as i] + [app.main.ui.keyboard :as kbd] + [app.main.ui.workspace.sidebar.layers :refer [element-icon layer-name frame-wrapper]])) + +(def selected-shapes + (l/derived (comp :selected :viewer-local) st/state)) + +(def page-ref + (l/derived (comp :page :viewer-data) st/state)) + +(defn- make-collapsed-iref + [id] + #(-> (l/in [:viewer-local :collapsed id]) + (l/derived st/state) )) + +(mf/defc layer-item + [{:keys [index item selected objects disable-collapse?] :as props}] + (let [id (:id item) + selected? (contains? selected id) + item-ref (mf/use-ref nil) + collapsed-iref (mf/use-memo + (mf/deps id) + (make-collapsed-iref id)) + + expanded? (not (mf/deref collapsed-iref)) + + toggle-collapse + (fn [event] + (dom/stop-propagation event) + (if (and expanded? (kbd/shift? event)) + (st/emit! (dv/collapse-all)) + (st/emit! (dv/toggle-collapse id)))) + + select-shape + (fn [event] + (dom/prevent-default event) + (let [id (:id item)] + (st/emit! (dv/select-shape id)) + #_(cond + (or (:blocked item) + (:hidden item)) + nil + + (.-shiftKey event) + (st/emit! (dv/select-shape id true)) + + (> (count selected) 1) + (st/emit! (dv/deselect-all) + (dv/select-shape id)) + :else + (st/emit! (dv/deselect-all) + (dv/select-shape id))))) + ] + + (mf/use-effect + (mf/deps selected) + (fn [] + (when (and (= (count selected) 1) selected?) + (.scrollIntoView (mf/ref-val item-ref) false)))) + + [:li {:ref item-ref + :class (dom/classnames + :component (not (nil? (:component-id item))) + :masked (:masked-group? item) + :selected selected?)} + + [:div.element-list-body {:class (dom/classnames :selected selected? + :icon-layer (= (:type item) :icon)) + :on-click select-shape} + [:& element-icon {:shape item}] + [:& layer-name {:shape item}] + + (when (and (not disable-collapse?) (:shapes item)) + [:span.toggle-content + {:on-click toggle-collapse + :class (when expanded? "inverse")} + i/arrow-slide])] + + (when (and (:shapes item) expanded?) + [:ul.element-children + (for [[index id] (reverse (d/enumerate (:shapes item)))] + (when-let [item (get objects id)] + [:& layer-item + {:item item + :selected selected + :index index + :objects objects + :key (:id item)}]))])])) + +(mf/defc layers-sidebar [{:keys [frame]}] + (let [page (mf/deref page-ref) + selected (mf/deref selected-shapes) + objects (:objects page)] + + [:aside.settings-bar.settings-bar-left + [:div.settings-bar-inside + [:ul.element-list + [:& layer-item + {:item frame + :selected selected + :index 0 + :objects objects + :disable-collapse? true}]]]])) diff --git a/frontend/src/app/main/ui/viewer/handoff/render.cljs b/frontend/src/app/main/ui/viewer/handoff/render.cljs new file mode 100644 index 000000000..5d55f462c --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/render.cljs @@ -0,0 +1,170 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.render + "The main container for a frame in handoff mode" + (:require + [rumext.alpha :as mf] + [app.util.object :as obj] + [app.util.dom :as dom] + [app.common.data :as d] + [app.common.pages :as cp] + [app.common.pages-helpers :as cph] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as geom] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.data.viewer :as dv] + [app.main.ui.shapes.filters :as filters] + [app.main.ui.shapes.circle :as circle] + [app.main.ui.shapes.frame :as frame] + [app.main.ui.shapes.group :as group] + [app.main.ui.shapes.icon :as icon] + [app.main.ui.shapes.image :as image] + [app.main.ui.shapes.path :as path] + [app.main.ui.shapes.rect :as rect] + [app.main.ui.shapes.text :as text] + [app.main.ui.viewer.handoff.selection-feedback :refer [selection-feedback]] + [app.main.ui.shapes.shape :refer [shape-container]])) + +(declare shape-container-factory) + +(defn handle-hover-shape [{:keys [type id]} hover?] + #(when-not (#{:group :frame} type) + (do + (dom/prevent-default %) + (dom/stop-propagation %) + (st/emit! (dv/hover-shape id hover?))))) + +(defn select-shape [{:keys [type id]}] + #(when-not (#{:group :frame} type) + (dom/prevent-default %) + (dom/stop-propagation %) + (st/emit! (dv/select-shape id)))) + +(defn shape-wrapper-factory + [component] + (mf/fnc shape-wrapper + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (unchecked-get props "childs") + frame (unchecked-get props "frame")] + + [:> shape-container {:shape shape + :on-mouse-enter (handle-hover-shape shape true) + :on-mouse-leave (handle-hover-shape shape false) + :on-click (select-shape shape)} + [:& component {:shape shape + :frame frame + :childs childs + :is-child-selected? true}]]))) + +(defn frame-container-factory + [objects] + (let [shape-container (shape-container-factory objects) + frame-shape (frame/frame-shape shape-container) + frame-wrapper (shape-wrapper-factory frame-shape)] + (mf/fnc frame-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (mapv #(get objects %) (:shapes shape)) + shape (geom/transform-shape shape) + + props (-> (obj/new) + (obj/merge! props) + (obj/merge! #js {:shape shape + :childs childs}))] + [:> frame-wrapper props])))) + +(defn group-container-factory + [objects] + (let [shape-container (shape-container-factory objects) + group-shape (group/group-shape shape-container) + group-wrapper (shape-wrapper-factory group-shape)] + (mf/fnc group-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + childs (mapv #(get objects %) (:shapes shape)) + props (-> (obj/new) + (obj/merge! props) + (obj/merge! #js {:childs childs}))] + [:> group-wrapper props])))) + +(defn shape-container-factory + [objects show-interactions?] + (let [path-wrapper (shape-wrapper-factory path/path-shape) + text-wrapper (shape-wrapper-factory text/text-shape) + icon-wrapper (shape-wrapper-factory icon/icon-shape) + rect-wrapper (shape-wrapper-factory rect/rect-shape) + image-wrapper (shape-wrapper-factory image/image-shape) + circle-wrapper (shape-wrapper-factory circle/circle-shape)] + (mf/fnc shape-container + {::mf/wrap-props false} + [props] + (let [shape (unchecked-get props "shape") + frame (unchecked-get props "frame") + group-container (mf/use-memo + (mf/deps objects) + #(group-container-factory objects))] + (when (and shape (not (:hidden shape))) + (let [shape (geom/transform-shape frame shape) + opts #js {:shape shape + :frame frame}] + (case (:type shape) + :curve [:> path-wrapper opts] + :text [:> text-wrapper opts] + :icon [:> icon-wrapper opts] + :rect [:> rect-wrapper opts] + :path [:> path-wrapper opts] + :image [:> image-wrapper opts] + :circle [:> circle-wrapper opts] + :group [:> group-container opts]))))))) + +(defn adjust-frame-position [frame-id objects] + (let [frame (get objects frame-id) + modifier (-> (gpt/point (:x frame) (:y frame)) + (gpt/negate) + (gmt/translate-matrix)) + + update-fn #(assoc-in %1 [%2 :modifiers :displacement] modifier) + modifier-ids (d/concat [frame-id] (cph/get-children frame-id objects))] + (reduce update-fn objects modifier-ids))) + +(defn make-vbox [frame] + (str "0 0 " (:width frame 0) " " (:height frame 0))) + +(mf/defc render-frame-svg + {::mf/wrap [mf/memo]} + [{:keys [objects frame-id zoom] :or {zoom 1} :as props}] + + (let [objects (adjust-frame-position frame-id objects) + frame (get objects frame-id) + width (* (:width frame) zoom) + height (* (:height frame) zoom) + vbox (make-vbox frame) + render-frame (mf/use-memo + (mf/deps objects) + #(frame-container-factory objects))] + + [:svg {:view-box vbox + :width width + :height height + :version "1.1" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :xmlns "http://www.w3.org/2000/svg"} + + [:& render-frame {:shape frame + :view-box vbox}] + + [:& selection-feedback {:frame frame}]])) + diff --git a/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs new file mode 100644 index 000000000..a3725b8d2 --- /dev/null +++ b/frontend/src/app/main/ui/viewer/handoff/selection_feedback.cljs @@ -0,0 +1,65 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.viewer.handoff.selection-feedback + (:require + [rumext.alpha :as mf] + [okulary.core :as l] + #_[app.util.object :as obj] + #_[app.common.data :as d] + #_[app.common.pages :as cp] + #_[app.common.pages-helpers :as cph] + #_[app.common.geom.matrix :as gmt] + #_[app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + #_[app.main.refs :as refs] + [app.main.store :as st] + #_[app.main.data.viewer :as dv] + #_[app.main.ui.shapes.filters :as filters] + #_[app.main.ui.shapes.circle :as circle] + #_[app.main.ui.shapes.frame :as frame] + #_[app.main.ui.shapes.group :as group] + #_[app.main.ui.shapes.icon :as icon] + #_[app.main.ui.shapes.image :as image] + #_[app.main.ui.shapes.path :as path] + #_[app.main.ui.shapes.rect :as rect] + #_[app.main.ui.shapes.text :as text] + #_[app.main.ui.shapes.shape :refer [shape-container]])) + +(def selection-rect-color-normal "#1FDEA7") +(def selection-rect-color-component "#00E0FF") +(def selection-rect-width 1) + +#_(def hover-ref + (l/derived (l/in [:viewer-local :hover]) st/state)) + +(defn make-hover-shapes-iref + [] + (let [hover->shapes + (fn [state] + (let [hover (get-in state [:viewer-local :hover]) + objects (get-in state [:viewer-data :page :objects]) + resolve-shape #(get objects %)] + (map resolve-shape hover)))] + #(l/derived hover->shapes st/state))) + +(mf/defc selection-feedback [{:keys [frame]}] + (let [hover-shapes-ref (mf/use-memo (make-hover-shapes-iref)) + hover-shapes (->> (mf/deref hover-shapes-ref) + (map #(gsh/translate-to-frame % frame)))] + (for [shape hover-shapes] + (let [{:keys [x y width height]} (:selrect shape)] + [:rect {:x x + :y y + :width width + :height height + :fill "transparent" + :stroke selection-rect-color-normal + :stroke-width selection-rect-width + :pointer-events "none"}])))) diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 44fddb528..4992e4b95 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -122,7 +122,7 @@ (t locale "viewer.header.share.create-link")])]]]])) (mf/defc header - [{:keys [data index local fullscreen? toggle-fullscreen] :as props}] + [{:keys [data index local fullscreen? toggle-fullscreen screen] :as props}] (let [{:keys [project file page frames]} data total (count frames) on-click #(st/emit! dv/toggle-thumbnails-panel) @@ -141,7 +141,14 @@ on-edit #(st/emit! (rt/nav :workspace {:project-id project-id :file-id file-id} - {:page-id page-id}))] + {:page-id page-id})) + + change-screen + (fn [screen] + (st/emit! + (rt/nav screen + {:file-id file-id :page-id page-id} + {:index index})))] [:header.viewer-header [:div.main-icon [:a {:on-click on-edit} i/logo-icon]] @@ -156,6 +163,14 @@ [:span.dropdown-button i/arrow-down] [:span.counters (str (inc index) " / " total)]] + [:div.mode-zone + [:button.mode-zone-button {:on-click #(when (not= screen :viewer) + (change-screen :viewer)) + :class (when (= screen :viewer) "active")} i/play] + [:button.mode-zone-button {:on-click #(when (not= screen :handoff) + (change-screen :handoff)) + :class (when (= screen :handoff) "active")} i/code]] + [:div.options-zone [:& interactions-menu {:interactions-mode interactions-mode}] diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index dc41b197d..57fb01152 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -94,7 +94,7 @@ [:span.name (:name frame)]]]) (mf/defc thumbnails-panel - [{:keys [data index] :as props}] + [{:keys [data index screen] :as props}] (let [expanded? (mf/use-state false) container (mf/use-ref) page-id (get-in data [:page :id]) @@ -111,7 +111,7 @@ on-item-click (fn [event index] (compare-and-set! selected false true) - (st/emit! (rt/nav :viewer {:file-id file-id + (st/emit! (rt/nav screen {:file-id file-id :page-id page-id} {:index index})) (when @expanded? (on-close)))] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 19fbad4b9..328ad3997 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -331,8 +331,7 @@ (mf/defc layers-toolbox {:wrap [mf/memo]} [] - (let [locale (mf/deref i18n/locale) - page (mf/deref refs/workspace-page)] + (let [page (mf/deref refs/workspace-page)] [:div#layers.tool-window [:div.tool-window-bar [:div.tool-window-icon i/layers]