diff --git a/resources/public/icons/timeline.svg b/resources/public/icons/timeline.svg new file mode 100644 index 00000000..8fc9f8b9 --- /dev/null +++ b/resources/public/icons/timeline.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/components.cljs b/src/renderer/components.cljs index 420147d6..55b22fcf 100644 --- a/src/renderer/components.cljs +++ b/src/renderer/components.cljs @@ -2,6 +2,7 @@ (:require ["@radix-ui/react-context-menu" :as ContextMenu] ["@radix-ui/react-dropdown-menu" :as DropdownMenu] + ["@radix-ui/react-switch" :as Switch] ["react-svg" :refer [ReactSVG]] [clojure.string :as str] [re-frame.core :as rf] @@ -17,6 +18,17 @@ props [renderer.components/icon icon]]) +(defn switch + [{:keys [id label default-checked? on-checked-change]}] + [:span.inline-flex.items-center + [:label.switch-label {:for id} label] + [:> Switch/Root + {:class "switch-root" + :id id + :default-checked default-checked? + :on-checked-change on-checked-change} + [:> Switch/Thumb {:class "switch-thumb"}]]]) + (defn shortcuts [event] (let [shortcuts @(rf/subscribe [:event-shortcuts event])] @@ -111,7 +123,6 @@ [:div.right-slot [shortcuts action]]])) - (defn dropdown-menu-item [{:keys [type label action checked?]}] (case type diff --git a/src/renderer/db.cljs b/src/renderer/db.cljs index 550ea4aa..15576c94 100644 --- a/src/renderer/db.cljs +++ b/src/renderer/db.cljs @@ -3,6 +3,7 @@ [renderer.document.db] [renderer.panel.db] [renderer.theme.db] + [renderer.timeline.db] [renderer.tree.db] [renderer.window.db])) @@ -24,7 +25,8 @@ [:tree renderer.tree.db/tree] [:panel [:map-of :key renderer.panel.db/panel]] [:window renderer.window.db/window] - [:theme [:mode renderer.theme.db/modes]]]) + [:theme [:mode renderer.theme.db/modes]] + [:timeline renderer.timeline.db/timeline]]) (def default {:tool :select @@ -63,4 +65,5 @@ :replay? true :grid-snap? false :guide-snap? true - :paused? false}}) + :paused? false + :speed 1}}) diff --git a/src/renderer/statusbar.cljs b/src/renderer/statusbar.cljs index f77670b3..60692c52 100644 --- a/src/renderer/statusbar.cljs +++ b/src/renderer/statusbar.cljs @@ -62,7 +62,11 @@ {:title "XML view" :active? [:panel/visible? :xml] :icon "code" - :action [:panel/toggle :xml]}]) + :action [:panel/toggle :xml]} + {:title "Timeline" + :active? [:panel/visible? :timeline] + :icon "timeline" + :action [:panel/toggle :timeline]}]) (defn set-zoom [e v] diff --git a/src/renderer/styles.css b/src/renderer/styles.css index b8ab526c..009fd1ee 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -504,3 +504,8 @@ pre { transform: translateX(19px); } } + +.switch-label { + @apply h-auto; + background: transparent; +} diff --git a/src/renderer/timeline/db.cljs b/src/renderer/timeline/db.cljs new file mode 100644 index 00000000..da4e6ee7 --- /dev/null +++ b/src/renderer/timeline/db.cljs @@ -0,0 +1,9 @@ +(ns renderer.timeline.db) + +(def timeline + [:map + [:time number?] + [:replay? boolean?] + [:grid-snap? boolean?] + [:guide-snap? boolean?] + [:paused? boolean?]]) diff --git a/src/renderer/timeline/events.cljs b/src/renderer/timeline/events.cljs index 0e78254a..c1a9aa9e 100644 --- a/src/renderer/timeline/events.cljs +++ b/src/renderer/timeline/events.cljs @@ -5,8 +5,8 @@ (defn svg-elements [] - (-> (dom/canvas-element) - (.querySelectorAll "svg"))) + (when-let [canvas (dom/canvas-element)] + (.querySelectorAll canvas "svg"))) (rf/reg-fx ::set-current-time @@ -18,6 +18,7 @@ (fn [] (doall (map #(.pauseAnimations %) (svg-elements))))) +#_:clj-kondo/ignore (rf/reg-fx ::unpause-animations (fn [] @@ -54,3 +55,8 @@ :timeline/toggle-replay (fn [db _] (update-in db [:timeline :replay?] not))) + + (rf/reg-event-db + :timeline/set-speed + (fn [db [_ speed]] + (assoc-in db [:timeline :speed] speed))) diff --git a/src/renderer/timeline/subs.cljs b/src/renderer/timeline/subs.cljs index 7e161cf3..9b030ddf 100644 --- a/src/renderer/timeline/subs.cljs +++ b/src/renderer/timeline/subs.cljs @@ -93,3 +93,8 @@ :timeline/replay? (fn [db _] (-> db :timeline :replay?))) + +(rf/reg-sub + :timeline/speed + (fn [db _] + (-> db :timeline :speed))) diff --git a/src/renderer/timeline/views.cljs b/src/renderer/timeline/views.cljs index 9419a7db..16d5a681 100644 --- a/src/renderer/timeline/views.cljs +++ b/src/renderer/timeline/views.cljs @@ -1,41 +1,77 @@ (ns renderer.timeline.views (:require - ["@radix-ui/react-switch" :as Switch] ["@xzdarcy/react-timeline-editor" :refer [Timeline]] + ["@radix-ui/react-select" :as Select] ["react" :as react] [re-frame.core :as rf] [reagent.core :as ra] [renderer.components :as comp])) +(def speed-options + [{:id :0.25 + :value 0.25 + :label "0.25x"} + {:id :0.5 + :value 0.5 + :label "0.5x"} + {:id :normal + :value 1 + :label "1x"} + {:id :1.5 + :value 1.5 + :label "1.5x"} + {:id :2 + :value 2 + :label "2x"}]) + +(defn speed-select + [editor-ref] + (let [speed @(rf/subscribe [:timeline/speed])] + [:div.inline-flex.items-center + [:label {:style {:height "auto" + :background "transparent"}} "Speed"] + [:> Select/Root {:value speed + :onValueChange #(.setPlayRate (.-current editor-ref) %)} + [:> Select/Trigger + {:class "select-trigger" + :aria-label "No a11y filter"} + [:> Select/Value {:placeholder "Filter"} + [:div.flex.gap-1.justify-between.items-center + {:style {:min-width "50px"}} + [:span (str speed "x")] + [:> Select/Icon {:class "select-icon"} + [comp/icon "chevron-down" {:class "small"}]]]]] + [:> Select/Portal + [:> Select/Content + {:class "menu-content rounded select-content"} + [:> Select/ScrollUpButton {:class "select-scroll-button"} + [comp/icon "chevron-up"]] + [:> Select/Viewport {:class "select-viewport"} + [:> Select/Group + (map (fn [{:keys [id value label]}] ^{:key id} + [:> Select/Item + {:value value + :class "menu-item select-item"} + [:> Select/ItemText label]]) speed-options)]] + [:> Select/ScrollDownButton + {:class "select-scroll-button"} + [comp/icon "chevron-down"]]]]]])) + (defn snap-controls [] (let [grid-snap? @(rf/subscribe [:timeline/grid-snap?]) guide-snap? @(rf/subscribe [:timeline/guide-snap?])] [:<> - [:span.inline-flex.items-center - [:label - {:for "grid-snap" - :style {:background "transparent" - :height "auto"}} - "Grid snap"] - [:> Switch/Root - {:class "switch-root" - :id "grid-snap" - :default-checked grid-snap? - :on-checked-change #(rf/dispatch [:timeline/set-grid-snap %])} - [:> Switch/Thumb {:class "switch-thumb"}]]] - [:span.inline-flex.items-center - [:label - {:for "guide-snap" - :style {:background "transparent" - :height "auto"}} - "Guide snap"] - [:> Switch/Root - {:class "switch-root" - :id "guide-snap" - :default-checked guide-snap? - :on-checked-change #(rf/dispatch [:timeline/set-guide-snap %])} - [:> Switch/Thumb {:class "switch-thumb"}]]]])) + [comp/switch + {:id "grid-snap" + :label "Grid snap" + :default-checked? grid-snap? + :on-checked-change #(rf/dispatch [:timeline/set-grid-snap %])}] + [comp/switch + {:id "guide-snap" + :label "Guide snap" + :default-checked? guide-snap? + :on-checked-change #(rf/dispatch [:timeline/set-guide-snap %])}]])) (defn toolbar [editor-ref] @@ -47,30 +83,30 @@ timeline? @(rf/subscribe [:panel/visible? :timeline])] [:div.toolbar.level-1.mb-px [:div.flex-1.flex - [comp/icon-button "go-to-start" {:on-click #(.setTime (.-current editor-ref) 0) - :disabled (zero? time)}] - (if paused? - [comp/icon-button "play" {:on-click #(.play (.-current editor-ref) #js {:autoEnd true})}] - [comp/icon-button "pause" {:on-click #(.pause (.-current editor-ref))}]) - [comp/icon-button "go-to-end" {:on-click #(.setTime (.-current editor-ref) end) - :disabled (>= time end)}] + [comp/icon-button "go-to-start" + {:on-click #(.setTime (.-current editor-ref) 0) + :disabled (zero? time)}] + [comp/radio-icon-button + {:title (if paused? "Play" "Pause") + :active? (not paused?) + :icon (if paused? "play" "pause") + :action #(if paused? + (.play (.-current editor-ref) #js {:autoEnd true}) + (.pause (.-current editor-ref)))}] + [comp/icon-button "go-to-end" + {:on-click #(.setTime (.-current editor-ref) end) + :disabled (>= time end)}] [comp/radio-icon-button {:title "Replay" :active? replay? :icon "refresh" :action #(rf/dispatch [:timeline/toggle-replay])}] + [speed-select editor-ref] [:span.p-2.font-mono time-formatted] (when timeline? [:<> [:span.v-divider] - [snap-controls]])] - [comp/toggle-icon-button - {:active? (not timeline?) - :active-icon "chevron-up" - :active-text "Show timeline" - :inactive-icon "times" - :inactive-text "Hide timeline" - :action #(rf/dispatch [:panel/toggle :timeline])}]])) + [snap-controls]])]])) (defn root [] @@ -88,7 +124,8 @@ (.setTime (.-current ref) 0) (.play (.-current ref) #js {:autoEnd true}))] ["afterSetTime" #(rf/dispatch-sync [:timeline/set-time (.-time %)])] - ["setTimeByTick" #(rf/dispatch-sync [:timeline/set-time (.-time %)])]]] + ["setTimeByTick" #(rf/dispatch-sync [:timeline/set-time (.-time %)])] + ["afterSetPlayRate" #(rf/dispatch [:timeline/set-speed (.-rate %)])]]] (.on (.-listener (.-current ref)) e f))) :component-will-unmount @@ -103,11 +140,12 @@ timeline? @(rf/subscribe [:panel/visible? :timeline])] [:div [toolbar ref] - [:> Timeline {:style {:height (if timeline? "200px" 0)} - :editor-data data - :effects effects - :ref ref - :grid-snap grid-snap? - :drag-line guide-snap? - :auto-scroll true - :on-click-action #(rf/dispatch [:element/select (keyword (.. %2 -action -id))])}]]))}))) + [:> Timeline + {:style {:height (if timeline? "200px" 0)} + :editor-data data + :effects effects + :ref ref + :grid-snap grid-snap? + :drag-line guide-snap? + :auto-scroll true + :on-click-action #(rf/dispatch [:element/select (keyword (.. %2 -action -id))])}]]))}))) diff --git a/src/renderer/views.cljs b/src/renderer/views.cljs index 0d595c84..933469af 100644 --- a/src/renderer/views.cljs +++ b/src/renderer/views.cljs @@ -76,7 +76,7 @@ {:style {:flex "0 1 30%"}}])] [status-bar/root] [history/tree]] - [timeline/root] + (when @(rf/subscribe [:panel/visible? :timeline]) [timeline/root]) [command-input]]])) (defn main-panel