diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index cefd382b72..0ab7d9ae75 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -62,6 +62,7 @@ [:map {:title "ProfileProps" :closed true} [:plugins {:optional true} ctp/schema:plugin-registry] [:renderer {:optional true} [::sm/one-of #{:svg :wasm}]] + [:japanese-layout-all-files {:optional true} ::sm/boolean] [:mcp-enabled {:optional true} ::sm/boolean] [:newsletter-updates {:optional true} ::sm/boolean] [:newsletter-news {:optional true} ::sm/boolean] diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index b0cda30dd8..7475e389e2 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -164,6 +164,21 @@ (t/is (nil? (:error out))) (t/is (nil? (:photo-id (:result out)))))))) +(t/deftest update-profile-japanese-layout-setting + (let [profile (th/create-profile* 1) + params {::th/type :update-profile-props + ::rpc/profile-id (:id profile) + :props {:japanese-layout-all-files true}} + out (th/command! params)] + (t/is (nil? (:error out))) + (t/is (true? (get-in out [:result :japanese-layout-all-files]))) + + (let [params {::th/type :get-profile + ::rpc/profile-id (:id profile)} + out (th/command! params)] + (t/is (nil? (:error out))) + (t/is (true? (get-in out [:result :props :japanese-layout-all-files])))))) + (t/deftest profile-deletion-1 (let [prof (th/create-profile* 1) file (th/create-file* 1 {:profile-id (:id prof) diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index ce45edd485..baffa8d91d 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -438,7 +438,12 @@ [:map {:title "SetTokensSource"} [:type [:= :set-tokens-source]] [:file-id ::sm/uuid] - [:library-id [:maybe ::sm/uuid]]]]]) + [:library-id [:maybe ::sm/uuid]]]] + + [:set-japanese-layout + [:map {:title "SetJapaneseLayout"} + [:type [:= :set-japanese-layout]] + [:enabled ::sm/boolean]]]]) (def schema:changes [:sequential {:gen/max 5 :gen/min 1} schema:change]) @@ -1092,6 +1097,10 @@ [data {:keys [library-id]}] (cfo/set-tokens-source data library-id)) +(defmethod process-change :set-japanese-layout + [data {:keys [enabled]}] + (ctf/set-japanese-layout-enabled data enabled)) + ;; === Operations (def decode-shape-attrs diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 3317d91653..d948196886 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -1150,6 +1150,18 @@ :library-id prev-val}) (apply-changes-local)))) +(defn set-japanese-layout + [changes enabled?] + (assert-file-data! changes) + (let [file-data (::file-data (meta changes)) + previous-enabled (ctf/japanese-layout-enabled? file-data)] + (-> changes + (update :redo-changes conj {:type :set-japanese-layout + :enabled enabled?}) + (update :undo-changes conj {:type :set-japanese-layout + :enabled previous-enabled}) + (apply-changes-local)))) + ;; Misc changes (defn reorder-children diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index df4733c25a..6d4914ee06 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -77,7 +77,8 @@ (def schema:options [:map {:title "FileOptions"} [:components-v2 {:optional true} ::sm/boolean] - [:base-font-size {:optional true} :string]]) + [:base-font-size {:optional true} :string] + [:japanese-layout {:optional true} ::sm/boolean]]) (def schema:data [:map {:title "FileData"} @@ -1255,6 +1256,19 @@ (assoc-in file-data [:options :base-font-size] base-font-size)) +;; Japanese text layout + +(defn japanese-layout-enabled? + [file-data] + (true? (get-in file-data [:options :japanese-layout]))) + +(defn set-japanese-layout-enabled + [file-data enabled?] + (if enabled? + (assoc-in file-data [:options :japanese-layout] true) + (d/update-in-when file-data [:options] dissoc :japanese-layout))) + + ;; Ref Chains (defn get-ref-chain-until-target-ref "Returns a vector with the shape ref chain until target-ref, including itself" diff --git a/common/test/common_tests/files_changes_test.cljc b/common/test/common_tests/files_changes_test.cljc index 935ff092fb..0d6dbc86f6 100644 --- a/common/test/common_tests/files_changes_test.cljc +++ b/common/test/common_tests/files_changes_test.cljc @@ -251,6 +251,26 @@ "The parent must use the updated bounds of both children") (assert-idempotent data changes)))))) +(t/deftest set-japanese-layout-file-option + (let [file-id (uuid/custom 2 2) + page-id (uuid/custom 1 1) + data (make-file-data file-id page-id) + enabled (ch/process-changes data [{:type :set-japanese-layout + :enabled true}]) + disabled (ch/process-changes enabled [{:type :set-japanese-layout + :enabled false}]) + changes (-> (pcb/empty-changes) + (pcb/with-file-data data) + (pcb/set-japanese-layout true))] + (t/is (false? (ctf/japanese-layout-enabled? data))) + (t/is (true? (ctf/japanese-layout-enabled? enabled))) + (t/is (false? (ctf/japanese-layout-enabled? disabled))) + (t/is (not (contains? (:options disabled) :japanese-layout))) + (t/is (= [{:type :set-japanese-layout :enabled true}] + (:redo-changes changes))) + (t/is (= '({:type :set-japanese-layout :enabled false}) + (:undo-changes changes))))) + (t/deftest add-obj (let [file-id (uuid/custom 2 2) page-id (uuid/custom 1 1) diff --git a/frontend/resources/images/icons/text-combine-upright-all.svg b/frontend/resources/images/icons/text-combine-upright-all.svg new file mode 100644 index 0000000000..594b7f49fd --- /dev/null +++ b/frontend/resources/images/icons/text-combine-upright-all.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/text-combine-upright-digits.svg b/frontend/resources/images/icons/text-combine-upright-digits.svg new file mode 100644 index 0000000000..3e367a3fd3 --- /dev/null +++ b/frontend/resources/images/icons/text-combine-upright-digits.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/text-combine-upright-none.svg b/frontend/resources/images/icons/text-combine-upright-none.svg new file mode 100644 index 0000000000..f54cf6f1e3 --- /dev/null +++ b/frontend/resources/images/icons/text-combine-upright-none.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/text-orientation-mixed.svg b/frontend/resources/images/icons/text-orientation-mixed.svg new file mode 100644 index 0000000000..72cd592478 --- /dev/null +++ b/frontend/resources/images/icons/text-orientation-mixed.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/text-orientation-upright.svg b/frontend/resources/images/icons/text-orientation-upright.svg new file mode 100644 index 0000000000..67d241b2bb --- /dev/null +++ b/frontend/resources/images/icons/text-orientation-upright.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/warichu-none.svg b/frontend/resources/images/icons/warichu-none.svg new file mode 100644 index 0000000000..7ede0d3d0a --- /dev/null +++ b/frontend/resources/images/icons/warichu-none.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/warichu.svg b/frontend/resources/images/icons/warichu.svg new file mode 100644 index 0000000000..3348550253 --- /dev/null +++ b/frontend/resources/images/icons/warichu.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/writing-mode-horizontal.svg b/frontend/resources/images/icons/writing-mode-horizontal.svg new file mode 100644 index 0000000000..204e9190e7 --- /dev/null +++ b/frontend/resources/images/icons/writing-mode-horizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/writing-mode-vertical.svg b/frontend/resources/images/icons/writing-mode-vertical.svg new file mode 100644 index 0000000000..e89a00a562 --- /dev/null +++ b/frontend/resources/images/icons/writing-mode-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index c8e538560f..ccd7c1ecae 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -679,6 +679,17 @@ (dm/export layout/toggle-layout-flag) (dm/export layout/remove-layout-flag) +(defn set-japanese-layout + [enabled?] + (ptk/reify ::set-japanese-layout + ptk/WatchEvent + (watch [it state _] + (let [file-data (dsh/lookup-file-data state) + changes (-> (pcb/empty-changes it) + (pcb/with-file-data file-data) + (pcb/set-japanese-layout enabled?))] + (rx/of (dch/commit-changes changes)))))) + ;; --- Profile (defn update-nudge diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 84f6bd72f2..ddf143cae0 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -11,6 +11,7 @@ [app.common.data.macros :as dm] [app.common.files.helpers :as cph] [app.common.files.tokens :as cfo] + [app.common.types.file :as ctf] [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.common.types.tokens-lib :as ctob] @@ -289,6 +290,14 @@ "Currently working file data on workspace" (l/derived dsh/lookup-file-data st/state)) +(def japanese-layout-config + "Japanese layout switches: `:file?` for the workspace file and + `:all-files?` from the profile." + (l/derived (fn [state] + {:file? (ctf/japanese-layout-enabled? (dsh/lookup-file-data state)) + :all-files? (true? (dm/get-in state [:profile :props :japanese-layout-all-files]))}) + st/state =)) + (def workspace-file-colors (l/derived (fn [{:keys [id] :as data}] (some-> (:colors data) (update-vals #(assoc % :file-id id)))) diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index 991532c963..1278e80fd3 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -276,6 +276,9 @@ (def ^:icon-id text-auto-height "text-auto-height") (def ^:icon-id text-auto-width "text-auto-width") (def ^:icon-id text-bottom "text-bottom") +(def ^:icon-id text-combine-upright-all "text-combine-upright-all") +(def ^:icon-id text-combine-upright-digits "text-combine-upright-digits") +(def ^:icon-id text-combine-upright-none "text-combine-upright-none") (def ^:icon-id text-fixed "text-fixed") (def ^:icon-id text-font-family "text-font-family") (def ^:icon-id text-font-size "text-font-size") @@ -287,6 +290,8 @@ (def ^:icon-id text-ltr "text-ltr") (def ^:icon-id text-middle "text-middle") (def ^:icon-id text-mixed "text-mixed") +(def ^:icon-id text-orientation-mixed "text-orientation-mixed") +(def ^:icon-id text-orientation-upright "text-orientation-upright") (def ^:icon-id text-palette "text-palette") (def ^:icon-id text-paragraph "text-paragraph") (def ^:icon-id text-rtl "text-rtl") @@ -309,7 +314,11 @@ (def ^:icon-id vertical-align-items-start "vertical-align-items-start") (def ^:icon-id view-as-icons "view-as-icons") (def ^:icon-id view-as-list "view-as-list") +(def ^:icon-id warichu "warichu") +(def ^:icon-id warichu-none "warichu-none") (def ^:icon-id wrap "wrap") +(def ^:icon-id writing-mode-horizontal "writing-mode-horizontal") +(def ^:icon-id writing-mode-vertical "writing-mode-vertical") (def icon-list "A collection of all icons" diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 7dec276e5d..9d285841ad 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -247,6 +247,9 @@ (def ^:icon text-auto-height (icon-xref :text-auto-height)) (def ^:icon text-auto-width (icon-xref :text-auto-width)) (def ^:icon text-bottom (icon-xref :text-bottom)) +(def ^:icon text-combine-upright-all (icon-xref :text-combine-upright-all)) +(def ^:icon text-combine-upright-digits (icon-xref :text-combine-upright-digits)) +(def ^:icon text-combine-upright-none (icon-xref :text-combine-upright-none)) (def ^:icon text-fixed (icon-xref :text-fixed)) (def ^:icon text-font-family (icon-xref :text-font-family)) (def ^:icon text-font-size (icon-xref :text-font-size)) @@ -258,6 +261,8 @@ (def ^:icon text-ltr (icon-xref :text-ltr)) (def ^:icon text-middle (icon-xref :text-middle)) (def ^:icon text-mixed (icon-xref :text-mixed)) +(def ^:icon text-orientation-mixed (icon-xref :text-orientation-mixed)) +(def ^:icon text-orientation-upright (icon-xref :text-orientation-upright)) (def ^:icon text-palette (icon-xref :text-palette)) (def ^:icon text-paragraph (icon-xref :text-paragraph)) (def ^:icon text-rtl (icon-xref :text-rtl)) @@ -284,7 +289,11 @@ (def ^:icon vertical-align-items-start (icon-xref :vertical-align-items-start)) (def ^:icon view-as-icons (icon-xref :view-as-icons)) (def ^:icon view-as-list (icon-xref :view-as-list)) +(def ^:icon warichu (icon-xref :warichu)) +(def ^:icon warichu-none (icon-xref :warichu-none)) (def ^:icon wrap (icon-xref :wrap)) +(def ^:icon writing-mode-horizontal (icon-xref :writing-mode-horizontal)) +(def ^:icon writing-mode-vertical (icon-xref :writing-mode-vertical)) (def default "A collection of all icons" diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index c4d711ba79..dee7ba1d0a 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -115,10 +115,35 @@ :on-change handle-render-change}]] [:> text* {:typography t/body-medium :class (stl/css :feedback)} [:a {:href "#" :on-click go-settings-feedback :class (stl/css :link)} (tr "dashboard.webgl-switch.feedback") [:> icon* {:icon-id "arrow-up-right" :size "s"}]]]])) +(mf/defc japanese-layout-settings* + [{:keys [enabled]}] + (let [handle-change + (mf/use-fn + (fn [enabled?] + (st/emit! (ev/event {::ev/name (if enabled? + "enable-japanese-layout-all-files" + "disable-japanese-layout-all-files") + ::ev/origin "settings"}) + (du/update-profile-props + {:japanese-layout-all-files enabled?}) + (ntf/success (tr "notifications.profile-saved")))))] + [:section {:class (stl/css :japanese-layout-container)} + [:> heading* {:class (stl/css :title) + :level 2 + :typography t/title-large} + (tr "dashboard.japanese-layout.title")] + [:> text* {:class (stl/css :description) :typography t/body-medium} + (tr "dashboard.japanese-layout.description")] + [:form {:class (stl/css :japanese-layout-form)} + [:> switch* {:label (tr "dashboard.japanese-layout.enable-all-files") + :default-checked enabled + :on-change handle-change}]]])) + (mf/defc options-page* [] (let [profile (mf/deref refs/profile) - renderer (or (-> profile :props :renderer) :svg)] + renderer (or (-> profile :props :renderer) :svg) + japanese-layout-enabled (:all-files? (mf/deref refs/japanese-layout-config))] (mf/use-effect #(dom/set-html-title (tr "title.settings.options"))) @@ -132,5 +157,6 @@ :id "options-section-title"} (tr "labels.settings")] [:> options-form*]] + [:> japanese-layout-settings* {:enabled japanese-layout-enabled}] (when (contains? cf/flags :render-switch) [:> webgl-settings* {:renderer renderer}])]])) diff --git a/frontend/src/app/main/ui/settings/options.scss b/frontend/src/app/main/ui/settings/options.scss index cdae1e185b..af13b89b95 100644 --- a/frontend/src/app/main/ui/settings/options.scss +++ b/frontend/src/app/main/ui/settings/options.scss @@ -21,9 +21,9 @@ } } -/* Copied from profile.scss .form-container, but without included nested - rules, since we want custom styles for it */ -.webgl-container { +/* Settings sections with standalone controls. */ +.webgl-container, +.japanese-layout-container { display: grid; grid-auto-rows: auto; gap: var(--sp-s); @@ -34,6 +34,10 @@ color: var(--color-foreground-primary); } +.japanese-layout-form { + padding-block-start: var(--sp-xs); +} + .webgl-header { display: flex; flex-direction: row; diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 79d2b80a20..93c5ffd753 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -45,6 +45,7 @@ [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] + [app.util.text.writing-mode :as wm] [beicon.v2.core :as rx] [potok.v2.core :as ptk] [rumext.v2 :as mf])) @@ -233,8 +234,10 @@ (mf/defc preferences-menu* {::mf/private true ::mf/wrap [mf/memo]} - [{:keys [layout profile toggle-flag on-close toggle-theme toggle-render]}] - (let [renderer (or (-> profile :props :renderer) :svg) + [{:keys [japanese-layout-enabled japanese-layout-all-files layout profile toggle-flag + on-close toggle-theme toggle-render toggle-japanese-layout]}] + (let [renderer (or (-> profile :props :renderer) :svg) + read-only? (mf/use-ctx ctx/workspace-read-only?) show-nudge-options (mf/use-fn @@ -317,6 +320,21 @@ :id "file-menu-nudge"} [:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]] + (when (and (not read-only?) + (not japanese-layout-all-files) + (wm/vertical-layout-active?)) + [:> dropdown-menu-item* {:on-click toggle-japanese-layout + :class (stl/css :base-menu-item :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-japanese-layout event))) + :data-testid "japanese-layout" + :id "file-menu-japanese-layout"} + [:span {:class (stl/css :item-name)} + (if japanese-layout-enabled + (tr "workspace.header.menu.disable-japanese-layout") + (tr "workspace.header.menu.enable-japanese-layout"))]]) + [:> dropdown-menu-item* {:on-click toggle-theme :class (stl/css :base-menu-item :submenu-item) :on-key-down (fn [event] @@ -886,6 +904,8 @@ (mf/defc menu* [{:keys [layout file]}] (let [profile (mf/deref refs/profile) + japanese-layout (mf/deref refs/japanese-layout-config) + japanese-layout-enabled (:file? japanese-layout) show-menu* (mf/use-state false) show-menu? (deref show-menu*) @@ -947,6 +967,15 @@ (dom/stop-propagation event) (st/emit! (du/toggle-theme)))) + toggle-japanese-layout + (mf/use-fn + (mf/deps japanese-layout-enabled) + (fn [event] + (dom/stop-propagation event) + (st/emit! (dw/set-japanese-layout (not japanese-layout-enabled))) + (reset! show-menu* false) + (reset! selected-sub-menu* nil))) + toggle-render (mf/use-fn (mf/deps profile) @@ -1146,10 +1175,13 @@ :preferences [:> preferences-menu* {:layout layout :profile profile + :japanese-layout-enabled japanese-layout-enabled + :japanese-layout-all-files (:all-files? japanese-layout) :toggle-flag toggle-flag :toggle-theme toggle-theme :toggle-render toggle-render :show-shortcuts show-shortcuts + :toggle-japanese-layout toggle-japanese-layout :on-close close-sub-menu}] :plugins diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs index ad8bcc2a9c..10b430c7a3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/common.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.common (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.main.data.workspace.tokens.application :as dwta] [app.main.store :as st] [app.util.dom :as dom] @@ -26,6 +27,20 @@ :ref ref} children]))) +(defn radio-selected + "Value to select in a radio group for an attribute value: its name, or + `default` when unset. Mixed (`:multiple`) values select nothing." + ([value] + (radio-selected value "")) + ([value default] + (cond + (= value :multiple) "" + (or (nil? value) + (and (string? value) (empty? value))) default + (keyword? value) (d/name value) + (string? value) value + :else (str value)))) + (defn emit-value-or-token [value emit-value-fn ids attrs] (if (or (string? value) (number? value) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 61ee90bf07..b571baed15 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -30,6 +30,8 @@ [app.main.ui.ds.controls.shared.searchable-options-dropdown :refer [searchable-options-dropdown*]] [app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.hooks :as hooks] + [app.main.ui.workspace.sidebar.options.common :refer [radio-selected]] + [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl] [app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]] [app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry*]] [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu] @@ -52,10 +54,6 @@ Evaluated once at module load time; cf/flags is immutable after startup." (contains? cf/flags :token-typography-row)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Sub-components -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (mf/defc text-align-options* [{:keys [values on-change on-blur]}] (let [options @@ -85,14 +83,14 @@ (when (some? on-blur) (on-blur))))] [:div {:class (stl/css :align-options)} - [:> radio-buttons* {:selected (:text-align values) + [:> radio-buttons* {:selected (radio-selected (:text-align values)) :on-change handle-change :name "align-text-options" :options options}]])) (mf/defc text-direction-options* [{:keys [values on-change on-blur]}] - (let [direction (:text-direction values) + (let [direction (radio-selected (:text-direction values)) options (mf/with-memo [] [{:value "ltr" @@ -119,7 +117,7 @@ (mf/defc vertical-align* [{:keys [values on-change on-blur]}] - (let [vertical-align (or (:vertical-align values) "top") + (let [vertical-align (radio-selected (:vertical-align values) "top") options (mf/with-memo [] [{:value "top" @@ -151,6 +149,7 @@ (mf/defc grow-options* [{:keys [ids values on-blur]}] (let [grow-type (:grow-type values) + selected (radio-selected grow-type) editor-instance (mf/deref refs/workspace-editor) options (mf/with-memo [] @@ -191,14 +190,14 @@ (when (some? on-blur) (on-blur))))] [:div {:class (stl/css :grow-options)} - [:> radio-buttons* {:selected (d/name grow-type) + [:> radio-buttons* {:selected selected :on-change handle-change :name "grow-text-options" :options options}]])) (mf/defc text-decoration-options* [{:keys [values on-change on-blur token-applied]}] - (let [text-decoration (some-> (:text-decoration values) d/name) + (let [text-decoration (radio-selected (:text-decoration values)) options (mf/with-memo [token-applied] [{:value "underline" @@ -222,8 +221,8 @@ [:div {:class (stl/css :text-decoration-options)} [:> radio-buttons* {:selected (if (= text-decoration "none") - nil - text-decoration) + "" + (radio-selected text-decoration)) :on-change handle-change :name "text-decoration-options" :disabled (and token-typography-row-enabled? (some? token-applied)) @@ -246,8 +245,10 @@ (d/seek #(= (:id %) (uuid/uuid id))))) (defn- check-props [n-props o-props] - (let [o-values (unchecked-get o-props "values") - n-values (unchecked-get n-props "values")] + (let [o-values (unchecked-get o-props "values") + n-values (unchecked-get n-props "values") + o-ruby-values (unchecked-get o-props "rubyValues") + n-ruby-values (unchecked-get n-props "rubyValues")] (and (identical? (unchecked-get n-props "ids") (unchecked-get o-props "ids")) (identical? (unchecked-get n-props "type") @@ -258,6 +259,9 @@ (unchecked-get o-props "fileId")) (identical? (unchecked-get n-props "typographies") (unchecked-get o-props "typographies")) + (identical? (unchecked-get n-props "textSelectionActive") + (unchecked-get o-props "textSelectionActive")) + (= n-ruby-values o-ruby-values) (identical? (get o-values :fills) (get n-values :fills)) (identical? (get o-values :font-family) @@ -284,6 +288,32 @@ (get n-values :text-decoration)) (identical? (get o-values :text-direction) (get n-values :text-direction)) + (identical? (get o-values :writing-mode) + (get n-values :writing-mode)) + (identical? (get o-values :text-orientation) + (get n-values :text-orientation)) + (identical? (get o-values :text-combine-upright) + (get n-values :text-combine-upright)) + (identical? (get o-values :warichu) + (get n-values :warichu)) + (identical? (get o-values :font-features) + (get n-values :font-features)) + (identical? (get o-values :annotation-clearance) + (get n-values :annotation-clearance)) + (identical? (get o-values :text-emphasis) + (get n-values :text-emphasis)) + (identical? (get o-values :ruby) + (get n-values :ruby)) + (identical? (get o-values :ruby-hidden) + (get n-values :ruby-hidden)) + (identical? (get o-values :ruby-size) + (get n-values :ruby-size)) + (identical? (get o-values :ruby-align) + (get n-values :ruby-align)) + (identical? (get o-values :ruby-overhang) + (get n-values :ruby-overhang)) + (identical? (get o-values :ruby-side) + (get n-values :ruby-side)) (identical? (get o-values :text-transform) (get n-values :text-transform)) (identical? (get o-values :typography-ref-file) @@ -291,7 +321,9 @@ (identical? (get o-values :typography-ref-id) (get n-values :typography-ref-id)) (identical? (get o-values :vertical-align) - (get n-values :vertical-align))))) + (get n-values :vertical-align)) + (identical? (get o-values :line-adjustment) + (get n-values :line-adjustment))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Main component @@ -299,14 +331,20 @@ (mf/defc text-menu* {::mf/wrap [#(mf/memo' % check-props)]} - [{:keys [ids type values applied-tokens libraries file-id typographies]}] + [{:keys [ids type values ruby-values text-selection-active + applied-tokens libraries file-id typographies]}] (let [;; --- UI state menu-state* (mf/use-state {:main-menu true - :more-options false}) + :more-options false + :japanese-layout true}) menu-state (deref menu-state*) main-menu-open? (:main-menu menu-state) more-options-open? (:more-options menu-state) + japanese-layout-open? (:japanese-layout menu-state) + + japanese-layout-config-enabled? + (tjl/japanese-layout-config-enabled? (mf/deref refs/japanese-layout-config)) font-id (or (:font-id values) (:font-id txt/default-typography)) @@ -397,6 +435,10 @@ (mf/use-fn #(swap! menu-state* update :more-options not)) + toggle-japanese-layout + (mf/use-fn + #(swap! menu-state* update :japanese-layout not)) + toggle-token-dropdown (mf/use-fn #(swap! token-dropdown-open* not)) @@ -427,6 +469,14 @@ (fn [attrs] (emit-update! ids attrs))) + on-ruby-presentation-change + (mf/use-fn + (mf/deps ids on-change text-selection-active) + (fn [attrs] + (if text-selection-active + (on-change attrs) + (st/emit! (dwt/update-all-ruby-presentation ids attrs))))) + on-convert-to-typography (mf/use-fn (mf/deps values ids file-id emit-update!) @@ -485,7 +535,17 @@ :values values :on-change on-change :show-recent true - :on-blur on-text-blur})] + :on-blur on-text-blur}) + + japanese-layout-props + (mf/props + {:ids ids + :values values + :ruby-values ruby-values + :text-selection-active text-selection-active + :on-change on-change + :on-ruby-presentation-change on-ruby-presentation-change + :on-blur on-text-blur})] (hooks/use-stream expand-stream @@ -503,97 +563,114 @@ (when token-dropdown-open? (ts/schedule 0 #(some-> (mf/ref-val dropdown-ref) dom/focus!)))) - [:section {:class (stl/css :element-set) - ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`). - :data-keep-editing-on-blur true - :aria-label (tr "workspace.options.text-options.text-section")} - [:div {:class (stl/css :element-title)} - [:> title-bar* {:collapsable true - :collapsed (not main-menu-open?) - :on-collapsed toggle-main-menu - :title label - :class (stl/css :title-spacing-text)} - [:* - (when (and token-typography-row-enabled? (some? (resolve-delay typography-tokens)) (not typography)) - [:> icon-button* {:variant "ghost" - :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") - :on-click toggle-token-dropdown - :tooltip-placement "top-left" - :icon i/tokens}]) - (when (and (not typography) (not multiple?) (not applied-token-name)) - [:> icon-button* {:variant "ghost" - :aria-label (if (not font) - (tr "workspace.options.font-not-available" (:font-family values)) - (tr "workspace.options.convert-to-typography")) - :on-click on-convert-to-typography - :tooltip-placement "top-left" - :disabled (not font) - :icon i/add}])]] - (when (and token-typography-row-enabled? token-dropdown-open?) - [:> searchable-options-dropdown* {:on-click on-option-click - :id listbox-id - :options (resolve-delay dropdown-options) - :selected selected-token-id - :align "right" - :placeholder (tr "workspace.tokens.search-by-token") - :ref set-option-ref}])] - - (when main-menu-open? - [:div {:class (stl/css :element-content)} - (cond - (and token-typography-row-enabled? (= :multiple current-token-name) (= typography-id :multiple)) - [:div {:class (stl/css :multiple-typography)} - [:span {:class (stl/css :multiple-text)} - (tr "workspace.libraries.text.mixed-tokens-and-assets")]] - - (and token-typography-row-enabled? (= :multiple current-token-name)) - [:div {:class (stl/css :multiple-typography)} - [:span {:class (stl/css :multiple-text)} - (tr "workspace.libraries.text.mixed-tokens")] - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.libraries.text.multiple-token-tooltip") + [:* + [:section {:class (stl/css :element-set) + ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`). + :data-keep-editing-on-blur true + :aria-label (tr "workspace.options.text-options.text-section")} + [:div {:class (stl/css :element-title)} + [:> title-bar* {:collapsable true + :collapsed (not main-menu-open?) + :on-collapsed toggle-main-menu + :title label + :class (stl/css :title-spacing-text)} + [:* + (when (and token-typography-row-enabled? (some? (resolve-delay typography-tokens)) (not typography)) + [:> icon-button* {:variant "ghost" + :aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown") + :on-click toggle-token-dropdown :tooltip-placement "top-left" - :on-click handle-detach-all-tokens - :icon i/detach}]] - - (and token-typography-row-enabled? current-token-name) - [:> token-typography-row* {:token-name current-token-name - :detach-token detach-token - :active-tokens (resolve-delay typography-tokens)}] - - (= typography-id :multiple) - [:div {:class (stl/css :multiple-typography)} - [:span {:class (stl/css :multiple-text)} - (tr "workspace.libraries.text.mixed-typography")] - [:> icon-button* {:variant "ghost" - :aria-label (tr "workspace.libraries.text.multiple-assets-tooltip") - :on-click handle-detach-typography + :icon i/tokens}]) + (when (and (not typography) (not multiple?) (not applied-token-name)) + [:> icon-button* {:variant "ghost" + :aria-label (if (not font) + (tr "workspace.options.font-not-available" (:font-family values)) + (tr "workspace.options.convert-to-typography")) + :on-click on-convert-to-typography :tooltip-placement "top-left" - :icon i/detach}]] + :disabled (not font) + :icon i/add}])]] + (when (and token-typography-row-enabled? token-dropdown-open?) + [:> searchable-options-dropdown* {:on-click on-option-click + :id listbox-id + :options (resolve-delay dropdown-options) + :selected selected-token-id + :align "right" + :placeholder (tr "workspace.tokens.search-by-token") + :ref set-option-ref}])] - typography - [:> typography-entry* {:file-id typography-file-id - :typography typography - :is-local (= typography-file-id file-id) - :on-detach handle-detach-typography - :on-change handle-change-typography}] + (when main-menu-open? + [:div {:class (stl/css :element-content)} + (cond + (and token-typography-row-enabled? (= :multiple current-token-name) (= typography-id :multiple)) + [:div {:class (stl/css :multiple-typography)} + [:span {:class (stl/css :multiple-text)} + (tr "workspace.libraries.text.mixed-tokens-and-assets")]] + (and token-typography-row-enabled? (= :multiple current-token-name)) + [:div {:class (stl/css :multiple-typography)} + [:span {:class (stl/css :multiple-text)} + (tr "workspace.libraries.text.mixed-tokens")] + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.libraries.text.multiple-token-tooltip") + :tooltip-placement "top-left" + :on-click handle-detach-all-tokens + :icon i/detach}]] + (and token-typography-row-enabled? current-token-name) + [:> token-typography-row* {:token-name current-token-name + :detach-token detach-token + :active-tokens (resolve-delay typography-tokens)}] - :else - [:> text-options* common-props]) + (= typography-id :multiple) + [:div {:class (stl/css :multiple-typography)} + [:span {:class (stl/css :multiple-text)} + (tr "workspace.libraries.text.mixed-typography")] + [:> icon-button* {:variant "ghost" + :aria-label (tr "workspace.libraries.text.multiple-assets-tooltip") + :on-click handle-detach-typography + :tooltip-placement "top-left" + :icon i/detach}]] - [:div {:class (stl/css :text-align-options)} - [:> text-align-options* common-props] - [:> grow-options* common-props] - [:> icon-button* {:variant "ghost" - :aria-label (tr "labels.options") - :data-testid "text-align-options-button" - :on-click toggle-more-options - :icon i/menu}]] + typography + [:> typography-entry* {:file-id typography-file-id + :typography typography + :is-local (= typography-file-id file-id) + :on-detach handle-detach-typography + :on-change handle-change-typography}] - (when more-options-open? - [:div {:class (stl/css :text-decoration-options)} - [:> vertical-align* common-props] - [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})] - [:> text-direction-options* common-props]])])])) + :else + [:> text-options* common-props]) + + [:div {:class (stl/css :text-align-options)} + [:> text-align-options* common-props] + [:> grow-options* common-props] + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.options") + :data-testid "text-align-options-button" + :on-click toggle-more-options + :icon i/menu}]] + + (when japanese-layout-config-enabled? + [:> tjl/line-adjustment-option* common-props]) + + (when more-options-open? + [:div {:class (stl/css :text-decoration-options)} + [:> vertical-align* common-props] + [:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})] + [:> text-direction-options* common-props]])])] + + (when japanese-layout-config-enabled? + [:section {:class (stl/css :element-set) + ;; Focusing these controls must not exit the v3 text editor (see `keep-editing-on-blur?`). + :data-keep-editing-on-blur true + :aria-label (tr "workspace.options.text-options.japanese-layout")} + [:div {:class (stl/css :element-title)} + [:> title-bar* {:collapsable true + :collapsed (not japanese-layout-open?) + :on-collapsed toggle-japanese-layout + :title (tr "workspace.options.text-options.japanese-layout") + :class (stl/css :title-spacing-text)}]] + (when japanese-layout-open? + [:div {:class (stl/css :element-content)} + [:> tjl/japanese-layout-options* japanese-layout-props]])])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs new file mode 100644 index 0000000000..8714f09158 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs @@ -0,0 +1,523 @@ +;; 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 Sucursal en España SL + +(ns app.main.ui.workspace.sidebar.options.menus.text-japanese-layout + (:require-macros [app.main.style :as stl]) + (:require + [app.common.types.text.cjk-layout :as cjk] + [app.main.ui.components.title-bar :refer [title-bar*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]] + [app.main.ui.ds.controls.select :refer [select*]] + [app.main.ui.ds.controls.switch :refer [switch*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.workspace.sidebar.options.common :refer [advanced-options* radio-selected]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.text.writing-mode :as wm] + [rumext.v2 :as mf])) + +(def ^:private mixed-span-values + #{:mixed :multiple "mixed" "multiple"}) + +(defn- mixed-span-value? + [value] + (contains? mixed-span-values value)) + +(defn- span-input-value + [value] + (if (mixed-span-value? value) + "mixed" + (radio-selected value))) + +(defn span-select-value + [value default] + (if (mixed-span-value? value) + "mixed" + (radio-selected value default))) + +(defn with-mixed-span-option + [options value] + (cond-> options + (mixed-span-value? value) + (conj {:id "mixed" + :label (tr "labels.mixed-values") + :disabled true + :dimmed true}))) + +(defn japanese-layout-config-enabled? + "True under the WASM renderer when Japanese layout is enabled for the file + or in the user's profile. `config` is a `refs/japanese-layout-config` + value." + [{:keys [file? all-files?]}] + (and ^boolean (wm/vertical-layout-active?) + (boolean (or file? all-files?)))) + +(defn vertical-japanese-layout? + "True when the Japanese layout controls are editing vertical text." + [values] + (= "vertical-rl" (:writing-mode values))) + +(defn proportional-metrics-feature + "Proportional metrics feature for the writing mode: `vpal` or `palt`." + [writing-mode] + (if (= writing-mode "vertical-rl") "vpal" "palt")) + +(defn text-combine-upright-options + [text-selection-active translate] + (cond-> [{:value "none" + :id "none-text-combine-upright" + :label (translate "workspace.options.text-options.text-combine-upright-none") + :icon i/text-combine-upright-none}] + text-selection-active + (conj {:value "all" + :id "all-text-combine-upright" + :label (translate "workspace.options.text-options.text-combine-upright-all") + :icon i/text-combine-upright-all}) + + true + (conj {:value "digits" + :id "digits-text-combine-upright" + :label (translate "workspace.options.text-options.text-combine-upright-digits") + :icon i/text-combine-upright-digits}))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Sub-components +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(mf/defc attr-radio-options* + "Radio group bound to one text attribute." + [{:keys [values on-change on-blur attr default-value name class options]}] + (let [selected (radio-selected (get values attr) default-value) + handle-change + (mf/use-fn + (mf/deps attr on-change on-blur) + (fn [value] + (on-change {attr value}) + (when (some? on-blur) (on-blur))))] + [:div {:class class} + [:> radio-buttons* {:selected selected + :on-change handle-change + :name name + :options options}]])) + +(defn writing-mode-options + [translate] + [{:value "horizontal-tb" + :id "horizontal-tb-writing-mode" + :label (translate "workspace.options.text-options.writing-mode-horizontal") + :icon i/writing-mode-horizontal} + {:value "vertical-rl" + :id "vertical-rl-writing-mode" + :label (translate "workspace.options.text-options.writing-mode-vertical") + :icon i/writing-mode-vertical}]) + +(defn text-orientation-options + [translate] + [{:value "mixed" + :id "mixed-text-orientation" + :label (translate "workspace.options.text-options.text-orientation-mixed") + :icon i/text-orientation-mixed} + {:value "upright" + :id "upright-text-orientation" + :label (translate "workspace.options.text-options.text-orientation-upright") + :icon i/text-orientation-upright}]) + +;; Warichu (割注): renders the selection as two half-size lines in one inline +;; position (top/bottom horizontally, right/left vertically). +(defn warichu-options + [translate] + [{:value "none" + :id "none-warichu" + :label (translate "workspace.options.text-options.warichu-none") + :icon i/warichu-none} + {:value "warichu" + :id "warichu-warichu" + :label (translate "workspace.options.text-options.warichu") + :icon i/warichu}]) + +;; select* matches options by :id, so each id is the stored value. +(defn text-combine-upright-count-options + [translate] + [{:id "digits2" + :label (translate "workspace.options.text-options.text-combine-upright-digits-2")} + {:id "digits3" + :label (translate "workspace.options.text-options.text-combine-upright-digits-3")} + {:id "digits" + :label (translate "workspace.options.text-options.text-combine-upright-digits-4")}]) + +(defn ruby-size-options + [translate] + [{:id "half" :label (translate "workspace.options.text-options.ruby-size-half")} + {:id "third" :label (translate "workspace.options.text-options.ruby-size-third")} + {:id "quarter" :label (translate "workspace.options.text-options.ruby-size-quarter")}]) + +(defn ruby-align-options + [translate] + [{:id "space-around" :label (translate "workspace.options.text-options.ruby-align-space-around")} + {:id "center" :label (translate "workspace.options.text-options.ruby-align-center")} + {:id "start" :label (translate "workspace.options.text-options.ruby-align-start")} + {:id "space-between" :label (translate "workspace.options.text-options.ruby-align-space-between")}]) + +(defn ruby-overhang-options + [translate] + [{:id "auto" :label (translate "workspace.options.text-options.ruby-overhang-auto")} + {:id "none" :label (translate "workspace.options.text-options.ruby-overhang-none")}]) + +(defn ruby-side-options + [translate] + [{:id "over" :label (translate "workspace.options.text-options.ruby-side-over")} + {:id "under" :label (translate "workspace.options.text-options.ruby-side-under")}]) + +(mf/defc text-combine-upright-options* + ;; Digit TCY finds eligible runs itself, so it applies to a whole shape; + ;; `all` is offered only for a text selection. + [{:keys [values on-change on-blur text-selection-active]}] + (let [text-combine-upright (radio-selected (:text-combine-upright values) + (cjk/enum-default :text-combine-upright)) + selected (if (cjk/digit-combine? text-combine-upright) "digits" text-combine-upright) + options + (mf/with-memo [text-selection-active] + (text-combine-upright-options text-selection-active tr)) + + handle-change + (mf/use-fn + (mf/deps on-change on-blur) + (fn [value] + (on-change {:text-combine-upright value}) + (when (some? on-blur) (on-blur))))] + + [:div {:class (stl/css :text-combine-upright-options)} + [:> radio-buttons* {:selected selected + :on-change handle-change + :name "text-combine-upright-options" + :options options}]])) + +(mf/defc text-combine-upright-count-options* + [{:keys [values on-change on-blur]}] + (let [text-combine-upright (radio-selected (:text-combine-upright values) + (cjk/enum-default :text-combine-upright)) + digits? (cjk/digit-combine? text-combine-upright) + options (mf/with-memo [] (text-combine-upright-count-options tr)) + + handle-change + (mf/use-fn + (mf/deps on-change on-blur) + (fn [value] + (on-change {:text-combine-upright value}) + (when (some? on-blur) (on-blur))))] + + (when digits? + [:div {:class (stl/css :japanese-select-option)} + [:span {:class (stl/css :japanese-option-label)} + (tr "workspace.options.text-options.text-combine-upright-digits-count")] + [:> select* {:default-selected text-combine-upright + :aria-label (tr "workspace.options.text-options.text-combine-upright-digits-count") + :options options + :on-change handle-change}]]))) + +(mf/defc ruby-options* + [{:keys [values on-change on-blur advanced-open on-toggle-advanced]}] + (let [ruby (span-input-value (:ruby values)) + ruby* (mf/use-state ruby) + dirty* (mf/use-state false) + value (deref ruby*) + dirty? (deref dirty*) + + commit! + (mf/use-fn + (mf/deps dirty? value on-change on-blur) + (fn [] + (when dirty? + (on-change {:ruby value}) + (reset! dirty* false)) + (when (some? on-blur) (on-blur)))) + + handle-change + (mf/use-fn + (fn [event] + (reset! dirty* true) + (reset! ruby* (dom/get-target-val event)))) + + handle-key-down + (mf/use-fn + (mf/deps commit!) + (fn [event] + (when (= "Enter" (.-key event)) + (dom/blur! (dom/get-target event)))))] + + (mf/with-effect [ruby] + (reset! ruby* ruby) + (reset! dirty* false)) + + [:div {:class (stl/css :ruby-options)} + [:> input* {:class (stl/css :ruby-input) + :label (tr "workspace.options.text-options.ruby") + :placeholder (tr "workspace.options.text-options.ruby-placeholder") + :value value + :on-change handle-change + :on-blur commit! + :on-key-down handle-key-down}] + [:> icon-button* {:variant "ghost" + :selected advanced-open + :aria-label (tr "workspace.options.text-options.ruby-advanced-options") + :aria-expanded advanced-open + :data-testid "ruby-advanced-options-toggle" + :on-click on-toggle-advanced + :icon i/menu}]])) + +(mf/defc span-select-option* + "Select bound to one span attribute, with a disabled entry for mixed values." + [{:keys [values on-change on-blur attr default-value label options]}] + (let [value (get values attr) + selected (span-select-value value default-value) + options (with-mixed-span-option options value) + handle-change + (mf/use-fn + (mf/deps attr on-change on-blur) + (fn [value] + (on-change {attr value}) + (when (some? on-blur) (on-blur))))] + [:div {:class (stl/css :japanese-select-option)} + [:span {:class (stl/css :japanese-option-label)} label] + [:> select* {:default-selected selected + :aria-label label + :options options + :on-change handle-change}]])) + +(defn- ruby-common-props + [values on-change on-blur] + (mf/props + {:values values + :on-change on-change + :on-blur on-blur})) + +(mf/defc ruby-hidden-option* + [{:keys [values on-change on-blur]}] + (let [value (:ruby-hidden values) + enabled? (cond + (mixed-span-value? value) nil + (true? value) true + :else false) + label (tr "workspace.options.text-options.ruby-hidden") + handle-change + (mf/use-fn + (mf/deps on-change on-blur) + (fn [checked?] + (on-change {:ruby-hidden checked?}) + (when (some? on-blur) (on-blur))))] + [:div {:class (stl/css :ruby-hidden-option :japanese-select-option)} + [:span {:class (stl/css :japanese-option-label)} label] + [:> switch* {:default-checked enabled? + :aria-label label + :on-change handle-change}]])) + +(mf/defc ruby-customization-options* + [{:keys [values on-change on-blur]}] + (let [common-props (ruby-common-props values on-change on-blur) + options (mf/with-memo [] + {:ruby-size (ruby-size-options tr) + :ruby-align (ruby-align-options tr) + :ruby-overhang (ruby-overhang-options tr) + :ruby-side (ruby-side-options tr)})] + [:div {:class (stl/css :japanese-layout-controls)} + [:> ruby-hidden-option* common-props] + [:> span-select-option* (mf/spread-props common-props + {:attr :ruby-size + :default-value (cjk/enum-default :ruby-size) + :label (tr "workspace.options.text-options.ruby-size") + :options (:ruby-size options)})] + [:> span-select-option* (mf/spread-props common-props + {:attr :ruby-align + :default-value (cjk/enum-default :ruby-align) + :label (tr "workspace.options.text-options.ruby-align") + :options (:ruby-align options)})] + [:> span-select-option* (mf/spread-props common-props + {:attr :ruby-overhang + :default-value (cjk/enum-default :ruby-overhang) + :label (tr "workspace.options.text-options.ruby-overhang") + :options (:ruby-overhang options)})] + [:> span-select-option* (mf/spread-props common-props + {:attr :ruby-side + :default-value (cjk/enum-default :ruby-side) + :label (tr "workspace.options.text-options.ruby-side") + :options (:ruby-side options)})]])) + +(mf/defc ruby-advanced-options* + [{:keys [values on-change on-blur]}] + (let [open* (mf/use-state false) + open? (deref open*) + toggle-open (mf/use-fn #(swap! open* not)) + common-props (ruby-common-props values on-change on-blur)] + [:div {:class (stl/css :ruby-advanced-options)} + [:> ruby-options* (mf/spread-props common-props + {:advanced-open open? + :on-toggle-advanced toggle-open})] + [:> advanced-options* {:class (stl/css :ruby-advanced-content) + :is-visible open?} + [:> ruby-customization-options* common-props]]])) + +(mf/defc ruby-presentation-options* + [{:keys [values on-change on-blur]}] + (let [open* (mf/use-state false) + open? (deref open*) + toggle-open (mf/use-fn #(swap! open* not)) + common-props (ruby-common-props values on-change on-blur)] + [:div {:class (stl/css :ruby-advanced-options)} + [:> title-bar* {:collapsable true + :collapsed (not open?) + :title (tr "workspace.options.text-options.ruby-advanced-options") + :on-collapsed toggle-open + :aria-expanded open? + :data-testid "ruby-presentation-options-toggle"}] + [:> advanced-options* {:class (stl/css :ruby-advanced-content) + :is-visible open?} + [:> ruby-customization-options* common-props]]])) + +(mf/defc font-features-options* + ;; Japanese proportional metric alternates, per span: `palt` for + ;; horizontal text, `vpal` for vertical. + [{:keys [values on-change on-blur]}] + (let [writing-mode (:writing-mode values) + feature (proportional-metrics-feature writing-mode) + value (:font-features values) + enabled? (cond + (mixed-span-value? value) nil + (= feature value) true + :else false) + + handle-change + (mf/use-fn + (mf/deps feature on-change on-blur) + (fn [checked?] + (on-change {:font-features (if checked? feature "none")}) + (when (some? on-blur) (on-blur))))] + + [:div {:class (stl/css :japanese-select-option)} + [:span {:class (stl/css :japanese-option-label)} + (tr "workspace.options.text-options.font-features")] + [:> switch* {:default-checked enabled? + :aria-label (tr "workspace.options.text-options.font-features") + :on-change handle-change}]])) + +(defn text-emphasis-options + ([] + (text-emphasis-options tr)) + ([translate] + [{:id "none" + :label (translate "workspace.options.text-options.text-emphasis-none")} + {:id "filled-dot" + :label (translate "workspace.options.text-options.text-emphasis-filled-dot")} + {:id "open-dot" + :label (translate "workspace.options.text-options.text-emphasis-open-dot")} + {:id "filled-circle" + :label (translate "workspace.options.text-options.text-emphasis-filled-circle")} + {:id "open-circle" + :label (translate "workspace.options.text-options.text-emphasis-open-circle")} + {:id "filled-sesame" + :label (translate "workspace.options.text-options.text-emphasis-filled-sesame")} + {:id "open-sesame" + :label (translate "workspace.options.text-options.text-emphasis-open-sesame")}])) + +(defn annotation-clearance-options + ([] + (annotation-clearance-options tr)) + ([translate] + [{:id "none" + :label (translate "workspace.options.text-options.annotation-clearance-none")} + {:id "auto" + :label (translate "workspace.options.text-options.annotation-clearance-auto")}])) + +(defn line-adjustment-options + ([] + (line-adjustment-options tr)) + ([translate] + [{:id "push-in-first" + :label (translate "workspace.options.text-options.line-adjustment-push-in-first")} + {:id "push-out-first" + :label (translate "workspace.options.text-options.line-adjustment-push-out-first")} + {:id "push-out-only" + :label (translate "workspace.options.text-options.line-adjustment-push-out-only")}])) + +(mf/defc line-adjustment-option* + "Whole-shape setting: how lines slightly too long are fitted." + [{:keys [values on-change on-blur]}] + (let [options (mf/with-memo [] (line-adjustment-options))] + [:> span-select-option* {:values values + :on-change on-change + :on-blur on-blur + :attr :line-adjustment + :default-value (cjk/enum-default :line-adjustment) + :label (tr "workspace.options.text-options.line-adjustment") + :options options}])) + +(mf/defc japanese-layout-options* + [{:keys [values ruby-values text-selection-active + on-change on-ruby-presentation-change on-blur]}] + (let [vertical? (vertical-japanese-layout? values) + common-props (mf/props + {:values values + :on-change on-change + :on-blur on-blur}) + ruby-presentation-props (mf/props + {:values ruby-values + :on-change on-ruby-presentation-change + :on-blur on-blur}) + options (mf/with-memo [] + {:writing-mode (writing-mode-options tr) + :text-orientation (text-orientation-options tr) + :warichu (warichu-options tr) + :text-emphasis (text-emphasis-options) + :annotation-clearance (annotation-clearance-options)})] + + [:div {:class (stl/css :japanese-layout-options)} + [:div {:class (stl/css :japanese-layout-controls)} + [:div {:class (stl/css :japanese-icon-options)} + [:> attr-radio-options* (mf/spread-props common-props + {:attr :writing-mode + :default-value (cjk/enum-default :writing-mode) + :name "writing-mode-options" + :class (stl/css :writing-mode-options) + :options (:writing-mode options)})] + (when ^boolean vertical? + [:* + ;; An unset orientation reads as nil, or "" in the v2 editor; both + ;; select "mixed". + [:> attr-radio-options* (mf/spread-props common-props + {:attr :text-orientation + :default-value (cjk/enum-default :text-orientation) + :name "text-orientation-options" + :class (stl/css :text-orientation-options) + :options (:text-orientation options)})] + [:> text-combine-upright-options* + (mf/spread-props common-props + {:text-selection-active text-selection-active})]]) + (when ^boolean text-selection-active + [:> attr-radio-options* (mf/spread-props common-props + {:attr :warichu + :default-value (cjk/enum-default :warichu) + :name "warichu-options" + :class (stl/css :warichu-options) + :options (:warichu options)})])] + (when ^boolean vertical? + [:> text-combine-upright-count-options* common-props]) + [:> font-features-options* common-props] + ;; Emphasis marks (圏点 / bouten) apply to the selected characters. + (when ^boolean text-selection-active + [:> span-select-option* (mf/spread-props common-props + {:attr :text-emphasis + :default-value (cjk/enum-default :text-emphasis) + :label (tr "workspace.options.text-options.text-emphasis") + :options (:text-emphasis options)})]) + [:> span-select-option* (mf/spread-props common-props + {:attr :annotation-clearance + :default-value (cjk/enum-default :annotation-clearance) + :label (tr "workspace.options.text-options.annotation-clearance") + :options (:annotation-clearance options)})] + (if text-selection-active + [:> ruby-advanced-options* common-props] + [:> ruby-presentation-options* ruby-presentation-props])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss new file mode 100644 index 0000000000..b84d9971a5 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss @@ -0,0 +1,78 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC Sucursal en España SL + +@use "ds/typography.scss" as t; +@use "ds/_sizes.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; + +.writing-mode-options, +.text-orientation-options, +.text-combine-upright-options, +.warichu-options { + block-size: $sz-32; +} + +.ruby-advanced-options { + display: flex; + flex-direction: column; + overflow: visible; +} + +.ruby-options { + display: flex; + align-items: flex-end; + gap: var(--sp-xs); +} + +.ruby-input { + flex: 1; +} + +.ruby-advanced-content { + display: flex; + flex-direction: column; + gap: var(--sp-xs); + padding: var(--sp-xs); + border: $b-1 solid var(--color-background-quaternary); + border-radius: $br-8; +} + +.japanese-layout-options, +.japanese-layout-controls { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.japanese-layout-options { + padding-block: var(--sp-xs); +} + +.ruby-hidden-option { + min-block-size: $sz-32; +} + +.japanese-icon-options { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-xs); +} + +.japanese-select-option { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); + align-items: center; + gap: var(--sp-s); + inline-size: 100%; +} + +.japanese-option-label { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs index dd2c9dc102..fe0fa3050d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/text.cljs @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.types.shape.layout :as ctl] [app.common.types.text :as txt] + [app.common.types.text.cjk-layout :as cjk] [app.main.data.workspace.texts :as dwt] [app.main.features :as features] [app.main.refs :as refs] @@ -27,8 +28,30 @@ [app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu*]] [app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu*]] [app.main.ui.workspace.sidebar.options.menus.text :refer [text-menu*]] + [app.render-wasm.api :as wasm.api] [rumext.v2 :as mf])) +(defn- dom-text-range-selected? + [editor] + (when editor + (let [selection (.getSelection js/window) + element (.-element editor) + anchor (some-> selection .-anchorNode) + focus (some-> selection .-focusNode)] + (and selection + (not (.-isCollapsed selection)) + element + anchor + focus + (.contains element anchor) + (.contains element focus))))) + +(defn- draft-text-range-selected? + [editor-state] + (when editor-state + (let [selection (.getSelection editor-state)] + (and selection (not (.isCollapsed selection)))))) + (mf/defc options* [{:keys [shape libraries file-id page-id]}] (let [id (dm/get-prop shape :id) @@ -128,6 +151,19 @@ (when text-editor-v2? editor) + text-selection-active + (boolean + (cond + text-editor-wasm? + (and (= id (wasm.api/text-editor-get-active-shape-id)) + (wasm.api/text-editor-has-selection?)) + + text-editor-v2? + (dom-text-range-selected? editor-instance) + + :else + (draft-text-range-selected? editor-state))) + fill-values (dwt/current-text-values {:editor-styles editor-styles @@ -154,7 +190,14 @@ :editor-state editor-state :editor-instance editor-instance :shape shape - :attrs txt/text-node-attrs}))] + :attrs txt/text-node-attrs})) + + ruby-values + (if text-selection-active + (select-keys text-values cjk/ruby-presentation-attrs) + (dwt/current-ruby-values + {:shape shape + :attrs cjk/ruby-presentation-attrs}))] [:* [:> layer-menu* {:ids ids @@ -202,6 +245,8 @@ :type type :applied-tokens applied-tokens :values text-values + :ruby-values ruby-values + :text-selection-active text-selection-active :libraries libraries :file-id file-id :typographies typographies}] @@ -237,4 +282,3 @@ :values (select-keys shape exports-attrs) :page-id page-id :file-id file-id}]])) - diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index f68303e117..19ee5bfd54 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -143,6 +143,7 @@ [frontend-tests.ui.static-test] [frontend-tests.ui.stroke-menu-test] [frontend-tests.ui.text-attrs-multiple-test] + [frontend-tests.ui.text-options-test] [frontend-tests.ui.workspace-libraries-test] [frontend-tests.util-clipboard-test] [frontend-tests.util-object-test] @@ -311,6 +312,7 @@ 'frontend-tests.ui.static-test 'frontend-tests.ui.stroke-menu-test 'frontend-tests.ui.text-attrs-multiple-test + 'frontend-tests.ui.text-options-test 'frontend-tests.ui.workspace-libraries-test 'frontend-tests.util-clipboard-test 'frontend-tests.util-object-test diff --git a/frontend/test/frontend_tests/ui/text_options_test.cljs b/frontend/test/frontend_tests/ui/text_options_test.cljs new file mode 100644 index 0000000000..e0999e870b --- /dev/null +++ b/frontend/test/frontend_tests/ui/text_options_test.cljs @@ -0,0 +1,248 @@ +;; 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 Sucursal en España SL + +(ns frontend-tests.ui.text-options-test + (:require + ["react-dom/server" :as rds] + [app.common.types.text.cjk-layout :as cjk] + [app.main.data.workspace.texts :as dwt] + [app.main.ui.ds.controls.select :as select] + [app.main.ui.shapes.text.html-text :as html-text] + [app.main.ui.shapes.text.styles :as text-styles] + [app.main.ui.workspace.sidebar.options.common :refer [radio-selected]] + [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl] + [app.util.text.writing-mode :as wm] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(def ^:private japanese-span-attrs + [:text-combine-upright + :text-emphasis + :ruby + :ruby-hidden + :ruby-size + :ruby-align + :ruby-overhang + :ruby-side + :warichu + :font-features + :annotation-clearance]) + + +(t/deftest japanese-layout-controls-follow-file-or-profile-configuration + (with-redefs [wm/vertical-layout-active? (constantly true)] + (t/is (false? (tjl/japanese-layout-config-enabled? {}))) + (t/is (false? (tjl/japanese-layout-config-enabled? {:file? false :all-files? false}))) + (t/is (true? (tjl/japanese-layout-config-enabled? {:file? true :all-files? false}))) + (t/is (true? (tjl/japanese-layout-config-enabled? {:file? false :all-files? true})))) + (with-redefs [wm/vertical-layout-active? (constantly false)] + (t/is (false? (tjl/japanese-layout-config-enabled? {:file? true :all-files? true}))))) + +(t/deftest text-emphasis-select-reports-and-restores-canonical-values + (let [options (tjl/text-emphasis-options identity) + reported-id (:id (select/get-option options "filled-dot"))] + (t/is (= "filled-dot" reported-id)) + (t/is (= "filled-dot" + (:id (select/get-option options reported-id)))))) + +(t/deftest annotation-clearance-select-reports-canonical-values + (let [options (tjl/annotation-clearance-options identity)] + (t/is (= ["none" "auto"] (mapv :id options))) + (t/is (= "auto" (:id (select/get-option options "auto")))))) + +(t/deftest unrestricted-tcy-is-only-offered-for-a-text-selection + (t/is (= ["none" "digits"] + (mapv :value (tjl/text-combine-upright-options false identity)))) + (t/is (= ["none" "all" "digits"] + (mapv :value (tjl/text-combine-upright-options true identity))))) + +(t/deftest ruby-container-styles-preserve-customization + (let [style (text-styles/generate-ruby-container-styles + {:ruby-align "space-between" + :ruby-overhang "none" + :ruby-side "under"})] + (t/is (= "space-between" (.-rubyAlign style))) + (t/is (= "none" (.-rubyOverhang style))) + (t/is (= "under" (.-rubyPosition style))))) + +(t/deftest advanced-furigana-options-are-collapsed-by-default + (let [markup (rds/renderToStaticMarkup + (mf/element tjl/ruby-presentation-options* + #js {:values {} + :on-change identity + :on-blur identity}))] + (t/is (str/includes? markup "data-testid=\"ruby-presentation-options-toggle\"")) + (t/is (str/includes? markup "aria-expanded=\"false\"")) + (t/is (str/includes? markup "workspace.options.text-options.ruby-advanced-options")))) + +(t/deftest advanced-furigana-options-include-hide-toggle + (let [markup (rds/renderToStaticMarkup + (mf/element tjl/ruby-hidden-option* + #js {:values {:ruby-hidden true} + :on-change identity + :on-blur identity}))] + (t/is (str/includes? markup "workspace.options.text-options.ruby-hidden")) + (t/is (str/includes? markup "role=\"switch\"")) + (t/is (str/includes? markup "aria-checked=\"true\"")))) + +(t/deftest hidden-furigana-renders-base-text-without-ruby-layout + (let [markup (rds/renderToStaticMarkup + (mf/element html-text/render-text* + #js {:node {:text "日" + :ruby "にち" + :ruby-hidden true} + :parent {} + :shape {}}))] + (t/is (str/includes? markup ">日")) + (t/is (not (str/includes? markup "