diff --git a/common/src/app/common/attrs.cljc b/common/src/app/common/attrs.cljc index 60e6e39eef..b76356572d 100644 --- a/common/src/app/common/attrs.cljc +++ b/common/src/app/common/attrs.cljc @@ -8,7 +8,11 @@ (:require [app.common.geom.shapes :as gsh] [app.common.math :as mth] - [app.common.types.text :as txt])) + [app.common.types.text :as txt] + [app.common.types.text.cjk-layout :as cjk])) + +(def ^:private language-paragraph-attrs + (set cjk/language-paragraph-attrs)) (defn- get-attr [obj attr] @@ -42,9 +46,9 @@ :multiple) (get obj attr ::unset))) - ;; An absent language is the plain profile, so it must participate - ;; when reporting mixed text selections. - (#{:text-language :korean-wrap} attr) + ;; A text without language differs from one with a language, so an + ;; absent value must count when reporting mixed text selections. + (contains? language-paragraph-attrs attr) (get obj attr) :else diff --git a/common/src/app/common/render_wasm/enums.clj b/common/src/app/common/render_wasm/enums.clj index b076d92cc7..56658ac98b 100644 --- a/common/src/app/common/render_wasm/enums.clj +++ b/common/src/app/common/render_wasm/enums.clj @@ -49,6 +49,7 @@ ["ruby-align" "RawRubyAlign"] ["ruby-overhang" "RawRubyOverhang"] ["ruby-side" "RawRubySide"] + ["korean-wrap" "RawKoreanWrap"] ["line-adjustment" "RawLineAdjustment"] ["text-decoration" "RawTextDecoration"] ["text-transform" "RawTextTransform"] diff --git a/common/src/app/common/render_wasm/text_content.cljs b/common/src/app/common/render_wasm/text_content.cljs index b36fe245e2..b1696f13e7 100644 --- a/common/src/app/common/render_wasm/text_content.cljs +++ b/common/src/app/common/render_wasm/text_content.cljs @@ -118,6 +118,7 @@ text-transform (sr/translate-text-transform (get paragraph :text-transform)) writing-mode (sr/translate-text-layout-enum :writing-mode (get paragraph :writing-mode)) text-orientation (sr/translate-text-layout-enum :text-orientation (get paragraph :text-orientation)) + korean-wrap (sr/translate-text-layout-enum :korean-wrap (get paragraph :korean-wrap)) line-height (serialize-line-height (get paragraph :line-height)) letter-spacing (serialize-letter-spacing (get paragraph :letter-spacing))] (-> offset @@ -127,7 +128,7 @@ (mem/write-u8 dview text-transform) (mem/write-u8 dview writing-mode) (mem/write-u8 dview text-orientation) - (mem/write-u8 dview (case (:korean-wrap paragraph) "spaces-only" 1 "keep-words" 2 0)) + (mem/write-u8 dview korean-wrap) ;; Alignment padding; must match RawParagraphData in Rust. (mem/write-u8 dview 0) (mem/write-f32 dview line-height) diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index 7b493580c0..4a9e1fa670 100644 --- a/common/src/app/common/types/shape/text.cljc +++ b/common/src/app/common/types/shape/text.cljc @@ -16,9 +16,9 @@ (def node-types #{"root" "paragraph-set" "paragraph"}) -;; Values of a Japanese layout enum attr; paragraphs set the orientation +;; Values of a CJK layout enum attr; paragraphs set the orientation ;; of the whole shape and spans carry a copy. -(defn- schema:jl-enum +(defn- schema:cjk-enum [attr] (into [:enum] (get cjk/enum-values attr))) @@ -31,7 +31,7 @@ [:map [:type [:= "root"]] [:key {:optional true} :string] - [:line-adjustment {:optional true} (schema:jl-enum :line-adjustment)] + [:line-adjustment {:optional true} (schema:cjk-enum :line-adjustment)] [:children [:vector {:min 1 :gen/max 2 :gen/min 1} [:map @@ -52,9 +52,9 @@ ;; Paragraph language selects typography; it never follows the UI locale. [:text-language {:optional true} schema:text-language] ;; Word mode keeps punctuation rules; retain the old POC value on reload. - [:korean-wrap {:optional true} [:enum "syllable" "spaces-only" "keep-words"]] - [:writing-mode {:optional true} (schema:jl-enum :writing-mode)] - [:text-orientation {:optional true} (schema:jl-enum :text-orientation)] + [:korean-wrap {:optional true} (schema:cjk-enum :korean-wrap)] + [:writing-mode {:optional true} (schema:cjk-enum :writing-mode)] + [:text-orientation {:optional true} (schema:cjk-enum :text-orientation)] [:text-decoration {:optional true} ::sm/text] [:text-transform {:optional true} ::sm/text] [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] @@ -71,21 +71,21 @@ [:font-style {:optional true} ::sm/text] [:font-weight {:optional true} ::sm/text] [:direction {:optional true} ::sm/text] - [:text-combine-upright {:optional true} (schema:jl-enum :text-combine-upright)] - [:text-emphasis {:optional true} (schema:jl-enum :text-emphasis)] - [:text-emphasis-position {:optional true} (schema:jl-enum :text-emphasis-position)] - [:text-underline-position {:optional true} (schema:jl-enum :text-underline-position)] + [:text-combine-upright {:optional true} (schema:cjk-enum :text-combine-upright)] + [:text-emphasis {:optional true} (schema:cjk-enum :text-emphasis)] + [:text-emphasis-position {:optional true} (schema:cjk-enum :text-emphasis-position)] + [:text-underline-position {:optional true} (schema:cjk-enum :text-underline-position)] [:ruby {:optional true} :string] [:ruby-hidden {:optional true} :boolean] [:ruby-zhuyin {:optional true} :boolean] - [:ruby-size {:optional true} (schema:jl-enum :ruby-size)] - [:ruby-align {:optional true} (schema:jl-enum :ruby-align)] - [:ruby-overhang {:optional true} (schema:jl-enum :ruby-overhang)] - [:ruby-side {:optional true} (schema:jl-enum :ruby-side)] - [:warichu {:optional true} (schema:jl-enum :warichu)] - [:font-features {:optional true} (schema:jl-enum :font-features)] - [:annotation-clearance {:optional true} (schema:jl-enum :annotation-clearance)] - [:text-orientation {:optional true} (schema:jl-enum :text-orientation)] + [:ruby-size {:optional true} (schema:cjk-enum :ruby-size)] + [:ruby-align {:optional true} (schema:cjk-enum :ruby-align)] + [:ruby-overhang {:optional true} (schema:cjk-enum :ruby-overhang)] + [:ruby-side {:optional true} (schema:cjk-enum :ruby-side)] + [:warichu {:optional true} (schema:cjk-enum :warichu)] + [:font-features {:optional true} (schema:cjk-enum :font-features)] + [:annotation-clearance {:optional true} (schema:cjk-enum :annotation-clearance)] + [:text-orientation {:optional true} (schema:cjk-enum :text-orientation)] [:text-decoration {:optional true} ::sm/text] [:text-transform {:optional true} ::sm/text] [:typography-ref-id {:optional true} [:maybe ::sm/uuid]] diff --git a/common/src/app/common/types/text.cljc b/common/src/app/common/types/text.cljc index caf62884fe..3bc50acef8 100644 --- a/common/src/app/common/types/text.cljc +++ b/common/src/app/common/types/text.cljc @@ -108,8 +108,8 @@ (d/concat-vec text-align-attrs text-direction-attrs - [:text-language :korean-wrap] - cjk/whole-shape-paragraph-attrs)) + cjk/language-paragraph-attrs + cjk/vertical-writing-attrs)) (def text-node-attrs (d/concat-vec diff --git a/common/src/app/common/types/text/cjk_layout.cljc b/common/src/app/common/types/text/cjk_layout.cljc index c5920edf55..de19d876a5 100644 --- a/common/src/app/common/types/text/cjk_layout.cljc +++ b/common/src/app/common/types/text/cjk_layout.cljc @@ -34,7 +34,10 @@ :ruby-side ["over" "under"] ;; Root attr: how lines slightly too long are fitted (Adobe's Kinsoku ;; Type). It decides squeezing; alignment decides the space left over. - :line-adjustment ["push-in-first" "push-out-first" "push-out-only"]}) + :line-adjustment ["push-in-first" "push-out-first" "push-out-only"] + ;; Korean line breaking: between syllables, or between words only. + ;; "spaces-only" is the former name of "keep-words", kept on reload. + :korean-wrap ["syllable" "spaces-only" "keep-words"]}) (def ^:private enum-value-sets (update-vals enum-values set)) @@ -49,15 +52,20 @@ [attr value] (contains? (get enum-value-sets attr) value)) -;; Paragraph attrs. They apply to the whole shape: every paragraph carries -;; the first one's value. -(def whole-shape-paragraph-attrs +;; Vertical writing paragraph attrs. They apply to the whole shape: every +;; paragraph carries the first one's value. +(def vertical-writing-attrs [:writing-mode :text-orientation]) ;; Paragraph attrs the text options write to every paragraph, whatever the ;; editor selection: a text has one writing mode and one language. (def whole-text-paragraph-attrs - (conj whole-shape-paragraph-attrs :text-language)) + (conj vertical-writing-attrs :text-language)) + +;; Paragraph attrs of the language. A text has one language, but Korean +;; wrapping can change per paragraph. +(def language-paragraph-attrs + [:text-language :korean-wrap]) ;; Ruby attrs that change how a reading looks, not the reading itself. (def ruby-presentation-attrs @@ -83,7 +91,7 @@ ;; Values of the paragraph attrs when a paragraph does not store them. (def paragraph-attr-defaults - (into {} (map (juxt identity enum-default)) whole-shape-paragraph-attrs)) + (into {} (map (juxt identity enum-default)) vertical-writing-attrs)) ;; Glyph of each emphasis style, per CSS `text-emphasis-style`. (def ^:private emphasis-mark-chars @@ -210,21 +218,23 @@ (>= (count text) 2)))) (defn whole-shape-attr - "Value of a whole-shape paragraph `attr` (see `whole-shape-paragraph-attrs`) + "Value of a whole-shape paragraph `attr` (see `whole-text-paragraph-attrs`) in a text content: stored per paragraph, the first paragraph's value decides. Nil when unset." [content attr] (dm/get-in content [:children 0 :children 0 attr])) -(defn content-writing-mode - "Writing mode of a text content, or nil when unset." +(defn stored-writing-mode + "Writing mode stored in a text content, whatever its language, or nil + when unset. `active-writing-mode` is the one the renderer uses." [content] (whole-shape-attr content :writing-mode)) -(defn vertical-text-content? - "True when the text content flows vertically (vertical-rl)." +(defn stored-vertical? + "True when the text content stores vertical writing (vertical-rl), + whatever its language." [content] - (= "vertical-rl" (content-writing-mode content))) + (= "vertical-rl" (stored-writing-mode content))) ;; --- Active language @@ -239,16 +249,31 @@ "ko" :korean nil))) -;; Span attrs each language renders; the others stay in the content and come +(def ^:private ruby-attrs + (into #{:ruby} ruby-presentation-attrs)) + +;; CJK attrs each language renders; the others stay in the content and come ;; back when the text takes that language again. -(def ^:private language-span-attrs - {:japanese #{:text-combine-upright :warichu :text-emphasis :annotation-clearance - :ruby :ruby-hidden :ruby-size :ruby-align :ruby-overhang :ruby-side - :font-features} - :chinese #{:text-emphasis :text-emphasis-position :text-underline-position - :annotation-clearance :ruby :ruby-zhuyin :ruby-hidden :ruby-size - :ruby-align :ruby-overhang :ruby-side} - :korean #{}}) +(def ^:private language-attrs + {:japanese {:root #{:line-adjustment} + :paragraph (set vertical-writing-attrs) + :span (into #{:text-combine-upright :warichu :text-emphasis + :annotation-clearance :font-features} + (disj ruby-attrs :ruby-zhuyin))} + :chinese {:root #{} + :paragraph (set vertical-writing-attrs) + :span (into #{:text-emphasis :text-emphasis-position + :text-underline-position :annotation-clearance} + ruby-attrs)} + :korean {:root #{} + :paragraph (conj (set vertical-writing-attrs) :korean-wrap) + :span #{}}}) + +;; Attrs some language renders, per level: the ones without language drop. +(def ^:private language-dependent-attrs + (reduce-kv (fn [result _ levels] (merge-with into result levels)) + {:root #{} :paragraph #{} :span #{}} + language-attrs)) (defn content-language "Language of a text content: the first paragraph's `:text-language`, as a @@ -256,30 +281,29 @@ [content] (whole-shape-attr content :text-language)) -(defn- active-span - [span family] - (let [kept (get language-span-attrs family #{})] - (reduce (fn [span attr] - (if (contains? kept attr) span (dissoc span attr))) - span - span-attrs))) +(defn- keep-language-attrs + "`node` without the CJK attrs that its language `family` does not render + at `level` (`:root`, `:paragraph` or `:span`)." + [node family level] + (let [kept (get-in language-attrs [family level] #{})] + (reduce (fn [node attr] + (if (contains? kept attr) node (dissoc node attr))) + node + (get language-dependent-attrs level)))) (defn- active-paragraph [paragraph default-language] (let [language (or (:text-language paragraph) default-language) family (language-family language)] - (cond-> (d/update-when paragraph :children (partial mapv #(active-span % family))) + (cond-> (-> paragraph + (d/update-when :children + (partial mapv #(keep-language-attrs % family :span))) + (keep-language-attrs family :paragraph)) (some? language) (assoc :text-language language) - (nil? family) - (d/without-keys whole-shape-paragraph-attrs) - (and (nil? family) (= "distribute" (:text-align paragraph))) - (assoc :text-align "left") - - (not= :korean family) - (dissoc :korean-wrap)))) + (assoc :text-align "left")))) (defn active-content "Text `content` as the renderer lays it out. A paragraph without language @@ -288,17 +312,17 @@ horizontal and not distributed. The stored content keeps every attr." [content default-language] (let [family (language-family (or (content-language content) default-language))] - (cond-> (d/update-when content :children - (partial mapv - (fn [paragraph-set] - (d/update-when paragraph-set :children - (partial mapv #(active-paragraph % default-language)))))) - (not= :japanese family) - (dissoc :line-adjustment)))) + (-> content + (d/update-when :children + (partial mapv + (fn [paragraph-set] + (d/update-when paragraph-set :children + (partial mapv #(active-paragraph % default-language)))))) + (keep-language-attrs family :root)))) (defn active-writing-mode "Writing mode the renderer lays `content` out with (see `active-content`): nil when unset or when the text has no CJK language." [content default-language] (when (some? (language-family (or (content-language content) default-language))) - (content-writing-mode content))) + (stored-writing-mode content))) diff --git a/common/test/common_tests/types/text_test.cljc b/common/test/common_tests/types/text_test.cljc index 7fd07f38f7..1338b8f90c 100644 --- a/common/test/common_tests/types/text_test.cljc +++ b/common/test/common_tests/types/text_test.cljc @@ -397,7 +397,7 @@ {:korean-wrap "keep-all"}]] (t/is (not (ctst/valid-content? (with-paragraph-attrs attrs))) (pr-str attrs))) (t/is (contains? (set cttx/paragraph-attrs) :text-language)) - (t/is (not (contains? (set cjk/whole-shape-paragraph-attrs) :text-language)))) + (t/is (not (contains? (set cjk/vertical-writing-attrs) :text-language)))) (t/deftest japanese-span-attrs-accept-supported-values (t/is (ctst/valid-content? diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 7393644cbc..5647f3a9aa 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -26,7 +26,7 @@ (generate-root-styles props node false)) ([{:keys [width height]} node code?] (let [valign (:vertical-align node "top") - writing-mode (cjk/content-writing-mode node) + writing-mode (cjk/stored-writing-mode node) base #js {:height (when-not code? (fmt/format-pixels height)) :width (when-not code? (fmt/format-pixels width)) :display "flex" @@ -138,7 +138,7 @@ (when (pos? room) (str "calc(" base " + " (fmt/format-number room) ")"))))) -(defn- add-japanese-text-styles! +(defn- add-cjk-text-styles! [style data] (let [text-combine-upright (:text-combine-upright data) text-emphasis (:text-emphasis data) @@ -271,7 +271,7 @@ (obj/set! "whiteSpace" "pre") :always - (add-japanese-text-styles! data))))) + (add-cjk-text-styles! data))))) (defn- ruby-font-size "Pixel size of a span's ruby text. Paragraphs set font-size 0, so a @@ -299,7 +299,7 @@ (obj/unset! "inlineSize"))) (defn- enum-or-default - "Value of the Japanese layout enum `attr` in `data`, or its default when + "Value of the CJK layout enum `attr` in `data`, or its default when unset or unknown." [data attr] (let [value (get data attr)] 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 a9b2a00a45..6911ff03a9 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 @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.types.text :as txt] + [app.common.types.text.cjk-layout :as cjk] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.workspace.libraries :as dwl] @@ -266,6 +267,14 @@ (->> (:typography tokens) (d/seek #(= (:id %) (uuid/uuid id))))) +;; CJK attrs the text options show: language, writing mode, readings and +;; the other span, paragraph and root attrs. +(def ^:private cjk-option-attrs + (d/concat-vec cjk/language-paragraph-attrs + cjk/vertical-writing-attrs + cjk/span-attrs + [:line-adjustment])) + (defn- check-props [n-props o-props] (let [o-values (unchecked-get o-props "values") n-values (unchecked-get n-props "values") @@ -314,42 +323,6 @@ (get n-values :text-decoration)) (identical? (get o-values :text-direction) (get n-values :text-direction)) - (identical? (get o-values :text-language) - (get n-values :text-language)) - (identical? (get o-values :korean-wrap) - (get n-values :korean-wrap)) - (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 :text-emphasis-position) - (get n-values :text-emphasis-position)) - (identical? (get o-values :text-underline-position) - (get n-values :text-underline-position)) - (identical? (get o-values :ruby) - (get n-values :ruby)) - (identical? (get o-values :ruby-zhuyin) - (get n-values :ruby-zhuyin)) - (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) @@ -358,8 +331,7 @@ (get n-values :typography-ref-id)) (identical? (get o-values :vertical-align) (get n-values :vertical-align)) - (identical? (get o-values :line-adjustment) - (get n-values :line-adjustment))))) + (every? #(identical? (get o-values %) (get n-values %)) cjk-option-attrs)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Main component 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_cjk_controls.cljs similarity index 58% rename from frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs rename to frontend/src/app/main/ui/workspace/sidebar/options/menus/text_cjk_controls.cljs index 8f2287a2c9..f435555a3b 100644 --- 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_cjk_controls.cljs @@ -4,7 +4,10 @@ ;; ;; Copyright (c) KALEIDOS INC Sucursal en España SL -(ns app.main.ui.workspace.sidebar.options.menus.text-japanese-layout +(ns app.main.ui.workspace.sidebar.options.menus.text-cjk-controls + "Controls the CJK language panels share: writing mode, readings (ruby), + emphasis marks and annotation positions. The panels of each language + are in `text-language-layout`." (:require-macros [app.main.style :as stl]) (:require [app.common.types.text.cjk-layout :as cjk] @@ -23,7 +26,7 @@ (def ^:private mixed-span-values #{:mixed :multiple "mixed" "multiple"}) -(defn- mixed-span-value? +(defn mixed-span-value? [value] (contains? mixed-span-values value)) @@ -48,41 +51,26 @@ :disabled true :dimmed true}))) -(defn vertical-japanese-layout? - "True when the Japanese layout controls are editing vertical text." +(defn vertical-writing? + "True when the option `values` are of vertical text. The panels only + show under a CJK language, so the writing mode alone decides." [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 +;; Generic controls ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(mf/defc option-row* + "Row of a language panel: a `label` and the control in `children`." + [{:keys [label children]}] + [:div {:class (stl/css :option-row)} + [:span {:class (stl/css :option-label)} label] + children]) + (mf/defc attr-radio-options* "Radio group bound to one text attribute." - [{:keys [values on-change on-blur attr default-value name class options]}] + [{:keys [values on-change on-blur attr default-value name options]}] (let [selected (radio-selected (get values attr) default-value) group-id (mf/use-id) options (mf/with-memo [options group-id] @@ -94,12 +82,34 @@ (fn [value] (on-change {attr value}) (when (some? on-blur) (on-blur))))] - [:div {:class class} + [:div {:class (stl/css :radio-options)} [:> radio-buttons* {:selected selected :on-change handle-change :name name :options options}]])) +(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))))] + [:> option-row* {:label label} + [:> select* {:default-selected selected + :aria-label label + :options options + :on-change handle-change}]])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Writing mode +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn writing-mode-options [translate] [{:value "horizontal-tb" @@ -123,65 +133,32 @@ :icon i/text-orientation-upright}]) (mf/defc writing-mode-controls* - [{:keys [values on-change on-blur]}] - (let [props (mf/props {:values values :on-change on-change :on-blur on-blur})] - [:div {:class (stl/css :japanese-icon-options)} + "Writing mode row, with the orientation for vertical text. `children` add + the radio groups of a language to the row." + [{:keys [values on-change on-blur children]}] + (let [props (mf/props {:values values :on-change on-change :on-blur on-blur}) + options (mf/with-memo [] + {:writing-mode (writing-mode-options tr) + :text-orientation (text-orientation-options tr)})] + [:div {:class (stl/css :icon-options)} [:> attr-radio-options* (mf/spread-props 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 tr)})] - (when (vertical-japanese-layout? values) + :options (:writing-mode options)})] + ;; An unset orientation reads as nil, or "" in the v2 editor; both + ;; select "mixed". + (when (vertical-writing? values) [:> attr-radio-options* (mf/spread-props 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 tr)})])])) + :options (:text-orientation options)})]) + children])) -;; 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")}]) - -;; --- Positions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Positions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Annotation positions store both axes (over or under the line, right or ;; left of the column), but only one applies to a writing mode: above/below @@ -267,54 +244,108 @@ choice (when (not= "auto" choice) choice)] (position-of-sides attr (if ^boolean vertical [block choice] [choice inline]))))) -(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)) - +(mf/defc position-select-option* + "Select of the position `attr` with the sides of the writing mode only." + [{:keys [values on-change on-blur attr vertical label]}] + (let [value (get values attr) + selected (span-select-value (position-choice attr vertical value) "auto") + options (mf/with-memo [attr vertical value] + (with-mixed-span-option (position-options attr vertical) value)) handle-change (mf/use-fn - (mf/deps on-change on-blur) - (fn [value] - (on-change {:text-combine-upright value}) + (mf/deps attr vertical value on-change on-blur) + (fn [choice] + (on-change {attr (position-value attr vertical value choice)}) (when (some? on-blur) (on-blur))))] + [:> option-row* {:label label} + [:> select* {:default-selected selected + :aria-label label + :options options + :on-change handle-change}]])) - [:div {:class (stl/css :text-combine-upright-options)} - [:> radio-buttons* {:selected selected - :on-change handle-change - :name "text-combine-upright-options" - :options options}]])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Emphasis marks and annotation clearance +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(mf/defc text-combine-upright-count-options* +(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")}])) + +(mf/defc text-emphasis-option* + "Emphasis mark (圏点, 着重号) of the spans." [{: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)) + (let [options (mf/with-memo [] (text-emphasis-options))] + [:> span-select-option* {:values values + :on-change on-change + :on-blur on-blur + :attr :text-emphasis + :default-value (cjk/enum-default :text-emphasis) + :label (tr "workspace.options.text-options.text-emphasis") + :options options}])) - 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))))] +(mf/defc annotation-clearance-option* + "Whether readings and emphasis marks add to the line height." + [{:keys [values on-change on-blur]}] + (let [options (mf/with-memo [] (annotation-clearance-options))] + [:> span-select-option* {:values values + :on-change on-change + :on-blur on-blur + :attr :annotation-clearance + :default-value (cjk/enum-default :annotation-clearance) + :label (tr "workspace.options.text-options.annotation-clearance") + :options options}])) - (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}]]))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Readings (ruby) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; A reading is furigana in Japanese and pinyin or zhuyin in Chinese. The +;; panel of each language passes its `labels`. + +(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")}]) (mf/defc ruby-options* + "Input of the reading of the selected span." [{:keys [values on-change on-blur advanced-open on-toggle-advanced labels]}] (let [ruby (span-input-value (:ruby values)) ruby* (mf/use-state ruby) @@ -352,8 +383,8 @@ [:div {:class (stl/css :ruby-options)} [:> input* {:class (stl/css :ruby-input) - :label (or (:input labels) (tr "workspace.options.text-options.ruby")) - :placeholder (or (:placeholder labels) (tr "workspace.options.text-options.ruby-placeholder")) + :label (:input labels) + :placeholder (:placeholder labels) :value value :on-change handle-change :on-blur commit! @@ -363,50 +394,31 @@ (when on-toggle-advanced [:> icon-button* {:variant "ghost" :selected advanced-open - :aria-label (or (:advanced labels) (tr "workspace.options.text-options.ruby-advanced-options")) + :aria-label (:advanced labels) :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) +(mf/defc ruby-hidden-option* + "Switch hiding the readings, unset when the spans differ." + [{: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 attr on-change on-blur) - (fn [value] - (on-change {attr value}) + (mf/deps on-change on-blur) + (fn [checked?] + (on-change {:ruby-hidden checked?}) (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}]])) - -(mf/defc position-select-option* - "Select of the position `attr` with the sides of the writing mode only." - [{:keys [values on-change on-blur attr vertical label]}] - (let [value (get values attr) - selected (span-select-value (position-choice attr vertical value) "auto") - options (mf/with-memo [attr vertical value] - (with-mixed-span-option (position-options attr vertical) value)) - handle-change - (mf/use-fn - (mf/deps attr vertical value on-change on-blur) - (fn [choice] - (on-change {attr (position-value attr vertical value choice)}) - (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}]])) + [:> option-row* {:label label} + [:> switch* {:default-checked enabled? + :aria-label label + :on-change handle-change}]])) (defn- ruby-common-props [values on-change on-blur labels vertical] @@ -417,26 +429,6 @@ :labels labels :vertical vertical})) -(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 labels vertical]}] (let [common-props (ruby-common-props values on-change on-blur labels vertical) @@ -444,7 +436,7 @@ {:ruby-size (ruby-size-options tr) :ruby-align (ruby-align-options tr) :ruby-overhang (ruby-overhang-options tr)})] - [:div {:class (stl/css :japanese-layout-controls)} + [:div {:class (stl/css :option-list)} [:> ruby-hidden-option* common-props] [:> span-select-option* (mf/spread-props common-props {:attr :ruby-size @@ -466,6 +458,7 @@ :label (tr "workspace.options.text-options.ruby-side")})]])) (mf/defc ruby-advanced-options* + "Reading input of a text selection, with its presentation options." [{:keys [values on-change on-blur labels vertical]}] (let [open* (mf/use-state false) open? (deref open*) @@ -480,6 +473,8 @@ [:> ruby-customization-options* common-props]]])) (mf/defc ruby-presentation-options* + "Presentation options of every reading of the texts, without a text + selection." [{:keys [values on-change on-blur labels vertical]}] (let [open* (mf/use-state false) open? (deref open*) @@ -488,159 +483,10 @@ [:div {:class (stl/css :ruby-advanced-options)} [:> title-bar* {:collapsable true :collapsed (not open?) - :title (or (:advanced labels) (tr "workspace.options.text-options.ruby-advanced-options")) + :title (:advanced labels) :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* - "Japanese options. `children` go below the writing mode row." - [{:keys [values ruby-values text-selection-active - on-change on-ruby-presentation-change on-blur children]}] - (let [vertical? (vertical-japanese-layout? values) - common-props (mf/props - {:values values - :on-change on-change - :on-blur on-blur - :vertical vertical?}) - ruby-presentation-props (mf/props - {:values ruby-values - :on-change on-ruby-presentation-change - :on-blur on-blur - :vertical vertical?}) - 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)})])] - children - (when ^boolean vertical? - [:> text-combine-upright-count-options* common-props]) - [:> font-features-options* common-props] - [:> line-adjustment-option* 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_cjk_controls.scss similarity index 79% rename from frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.scss rename to frontend/src/app/main/ui/workspace/sidebar/options/menus/text_cjk_controls.scss index b84d9971a5..a3801d0aab 100644 --- 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_cjk_controls.scss @@ -9,13 +9,38 @@ @use "ds/_borders.scss" as *; @use "ds/spacing.scss" as *; -.writing-mode-options, -.text-orientation-options, -.text-combine-upright-options, -.warichu-options { +.radio-options { block-size: $sz-32; } +.icon-options { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-xs); +} + +.option-list { + display: flex; + flex-direction: column; + gap: var(--sp-xs); +} + +.option-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); + align-items: center; + gap: var(--sp-s); + inline-size: 100%; + min-block-size: $sz-32; +} + +.option-label { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); +} + .ruby-advanced-options { display: flex; flex-direction: column; @@ -40,39 +65,3 @@ 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/menus/text_language_layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs index e465d7794c..21c4b6632b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs @@ -5,6 +5,8 @@ ;; Copyright (c) KALEIDOS SUBSIDIARY SL (ns app.main.ui.workspace.sidebar.options.menus.text-language-layout + "Language panel of the text options, and the options of each CJK + language below it. The controls they share are in `text-cjk-controls`." (:require-macros [app.main.style :as stl]) (:require [app.common.types.text.cjk-layout :as cjk] @@ -18,11 +20,253 @@ [app.main.ui.ds.controls.switch :refer [switch*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [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.text-cjk-controls :as tcc] [app.util.i18n :refer [tr]] [app.util.text.layout :as tl] [rumext.v2 :as mf])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Japanese +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(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}))) + +;; 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")}]) + +;; 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}]) + +(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")}])) + +(defn- text-combine-upright-value + [values] + (radio-selected (:text-combine-upright values) (cjk/enum-default :text-combine-upright))) + +(mf/defc japanese-writing-mode-options* + "Radio groups Japanese adds to the writing mode row: tate-chu-yoko for + vertical text, warichu for a text selection. Digit tate-chu-yoko 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 [props (mf/props {:on-change on-change :on-blur on-blur}) + value (:text-combine-upright values) + ;; The digit counts are picked in their own select. + tcy (if (cjk/digit-combine? (radio-selected value)) "digits" value) + options (mf/with-memo [text-selection-active] + {:text-combine-upright (text-combine-upright-options text-selection-active tr) + :warichu (warichu-options tr)})] + [:* + (when (tcc/vertical-writing? values) + [:> tcc/attr-radio-options* (mf/spread-props props + {:values {:text-combine-upright tcy} + :attr :text-combine-upright + :default-value (cjk/enum-default :text-combine-upright) + :name "text-combine-upright-options" + :options (:text-combine-upright options)})]) + (when ^boolean text-selection-active + [:> tcc/attr-radio-options* (mf/spread-props props + {:values values + :attr :warichu + :default-value (cjk/enum-default :warichu) + :name "warichu-options" + :options (:warichu options)})])])) + +(mf/defc text-combine-upright-count-option* + [{:keys [values on-change on-blur]}] + (let [value (text-combine-upright-value values) + options (mf/with-memo [] (text-combine-upright-count-options tr))] + (when (cjk/digit-combine? value) + [:> tcc/span-select-option* {:values {:text-combine-upright value} + :on-change on-change + :on-blur on-blur + :attr :text-combine-upright + :default-value value + :label (tr "workspace.options.text-options.text-combine-upright-digits-count") + :options options}]))) + +(mf/defc font-features-option* + "Proportional metric alternates, per span: `palt` for horizontal text, + `vpal` for vertical." + [{:keys [values on-change on-blur]}] + (let [feature (proportional-metrics-feature (:writing-mode values)) + value (:font-features values) + enabled? (cond + (tcc/mixed-span-value? value) nil + (= feature value) true + :else false) + label (tr "workspace.options.text-options.font-features") + + 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))))] + + [:> tcc/option-row* {:label label} + [:> switch* {:default-checked enabled? + :aria-label label + :on-change handle-change}]])) + +(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))] + [:> tcc/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}])) + +(defn- japanese-reading-labels + [] + {:input (tr "workspace.options.text-options.ruby") + :placeholder (tr "workspace.options.text-options.ruby-placeholder") + :advanced (tr "workspace.options.text-options.ruby-advanced-options")}) + +(mf/defc japanese-layout-options* + "Japanese options below the writing mode row." + [{:keys [values ruby-values text-selection-active on-change on-ruby-presentation-change on-blur]}] + (let [vertical? (tcc/vertical-writing? values) + labels (japanese-reading-labels) + props (mf/props {:values values :on-change on-change :on-blur on-blur + :vertical vertical? :labels labels})] + [:* + (when ^boolean vertical? + [:> text-combine-upright-count-option* props]) + [:> font-features-option* props] + [:> line-adjustment-option* props] + ;; Emphasis marks (圏点 / bouten) apply to the selected characters. + (when ^boolean text-selection-active + [:> tcc/text-emphasis-option* props]) + [:> tcc/annotation-clearance-option* props] + (if ^boolean text-selection-active + [:> tcc/ruby-advanced-options* props] + [:> tcc/ruby-presentation-options* + (mf/spread-props props {:values ruby-values + :on-change on-ruby-presentation-change})])])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Chinese +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(mf/defc chinese-decoration-options* + [{:keys [values on-change on-blur]}] + (let [props (mf/props {:values values :on-change on-change :on-blur on-blur + :vertical (tcc/vertical-writing? values)})] + [:* + [:> tcc/text-emphasis-option* props] + [:> tcc/position-select-option* + (mf/spread-props props {:attr :text-emphasis-position + :label (tr "workspace.options.text-options.emphasis-position")})] + [:> tcc/position-select-option* + (mf/spread-props props {:attr :text-underline-position + :label (tr "workspace.options.text-options.underline-position")})] + [:> tcc/annotation-clearance-option* props]])) + +(defn- chinese-reading-labels + [zhuyin?] + (if zhuyin? + {:input (tr "workspace.options.text-options.zhuyin") + :placeholder (tr "workspace.options.text-options.zhuyin-placeholder") + :advanced (tr "workspace.options.text-options.pinyin-advanced-options") + :validate cjk/zhuyin-reading? + :invalid (tr "workspace.options.text-options.zhuyin-invalid")} + {:input (tr "workspace.options.text-options.pinyin") + :placeholder (tr "workspace.options.text-options.pinyin-placeholder") + :advanced (tr "workspace.options.text-options.pinyin-advanced-options")})) + +(mf/defc chinese-reading-options* + "Pinyin or zhuyin readings. Zhuyin annotates one Han character at a time." + [{:keys [values ruby-values text-selection-active reading-selection-active zhuyin-selection-active + on-change on-ruby-presentation-change on-blur]}] + (let [zhuyin? (true? (:ruby-zhuyin values)) + format-label (tr "workspace.options.text-options.chinese-reading-format") + handle-format-change (mf/use-fn + (mf/deps on-change) + (fn [value] (on-change {:ruby-zhuyin (= "zhuyin" value)}))) + props (mf/props {:values values :on-change on-change :on-blur on-blur + :vertical (tcc/vertical-writing? values) + :labels (chinese-reading-labels zhuyin?)}) + presentation-props (mf/spread-props props {:values ruby-values + :on-change on-ruby-presentation-change})] + [:* + (when reading-selection-active + [:> tcc/option-row* {:label format-label} + [:> select* {:default-selected (if zhuyin? "zhuyin" "pinyin") + :aria-label format-label + :options [{:id "pinyin" :label (tr "workspace.options.text-options.pinyin")} + {:id "zhuyin" :label (tr "workspace.options.text-options.zhuyin") + :disabled (not zhuyin-selection-active)}] + :on-change handle-format-change}]]) + (if (and reading-selection-active (or (not zhuyin?) zhuyin-selection-active)) + (if zhuyin? + [:* [:p {:class (stl/css :selection-hint)} (tr "workspace.options.text-options.zhuyin-selection-hint")] + [:> tcc/ruby-options* props] + [:> tcc/ruby-hidden-option* props]] + [:> tcc/ruby-advanced-options* props]) + [:* + [:p {:class (stl/css :selection-hint)} + (if zhuyin? (tr "workspace.options.text-options.zhuyin-selection-hint") (tr "workspace.options.text-options.pinyin-selection-hint"))] + (when-not text-selection-active + (if zhuyin? + [:> tcc/ruby-hidden-option* presentation-props] + [:> tcc/ruby-presentation-options* presentation-props]))])])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Korean +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn korean-wrap-options [value translate] (cond-> [{:id "syllable" :label (translate "workspace.options.text-options.korean-wrap-syllable")} @@ -30,67 +274,19 @@ (= value "spaces-only") (conj {:id "spaces-only" :label (translate "workspace.options.text-options.korean-wrap-spaces-only")}))) -(mf/defc chinese-decoration-options* +(mf/defc korean-layout-options* [{:keys [values on-change on-blur]}] - (let [props (mf/props {:values values :on-change on-change :on-blur on-blur - :vertical (tjl/vertical-japanese-layout? values)})] - [:div {:class (stl/css :decoration-options)} - [:> tjl/span-select-option* - (mf/spread-props props {:attr :text-emphasis :default-value "none" - :label (tr "workspace.options.text-options.text-emphasis") - :options (tjl/text-emphasis-options tr)})] - [:> tjl/position-select-option* - (mf/spread-props props {:attr :text-emphasis-position - :label (tr "workspace.options.text-options.emphasis-position")})] - [:> tjl/position-select-option* - (mf/spread-props props {:attr :text-underline-position - :label (tr "workspace.options.text-options.underline-position")})] - [:> tjl/span-select-option* - (mf/spread-props props {:attr :annotation-clearance :default-value "none" - :label (tr "workspace.options.text-options.annotation-clearance") - :options (tjl/annotation-clearance-options)})]])) + [:> tcc/span-select-option* {:values values + :on-change on-change + :on-blur on-blur + :attr :korean-wrap + :default-value (cjk/enum-default :korean-wrap) + :label (tr "workspace.options.text-options.korean-wrap") + :options (korean-wrap-options (:korean-wrap values) tr)}]) -(mf/defc chinese-reading-options* - [{:keys [values ruby-values text-selection-active reading-selection-active zhuyin-selection-active - on-change on-ruby-presentation-change on-blur]}] - (let [zhuyin? (true? (:ruby-zhuyin values)) - labels {:input (if zhuyin? (tr "workspace.options.text-options.zhuyin") (tr "workspace.options.text-options.pinyin")) - :placeholder (if zhuyin? (tr "workspace.options.text-options.zhuyin-placeholder") (tr "workspace.options.text-options.pinyin-placeholder")) - :advanced (tr "workspace.options.text-options.pinyin-advanced-options")} - handle-format-change (mf/use-fn - (mf/deps on-change) - (fn [value] (on-change {:ruby-zhuyin (= "zhuyin" value)}))) - props (mf/props {:values values :on-change on-change :on-blur on-blur - :vertical (tjl/vertical-japanese-layout? values) - :labels (cond-> labels zhuyin? - (assoc :validate cjk/zhuyin-reading? - :invalid (tr "workspace.options.text-options.zhuyin-invalid")))})] - [:div {:class (stl/css :pinyin-options)} - (when reading-selection-active - [:div {:class (stl/css :reading-format-option)} - [:span {:class (stl/css :option-label)} (tr "workspace.options.text-options.chinese-reading-format")] - [:> select* {:default-selected (if zhuyin? "zhuyin" "pinyin") - :aria-label (tr "workspace.options.text-options.chinese-reading-format") - :options [{:id "pinyin" :label (tr "workspace.options.text-options.pinyin")} - {:id "zhuyin" :label (tr "workspace.options.text-options.zhuyin") - :disabled (not zhuyin-selection-active)}] - :on-change handle-format-change}]]) - (if (and reading-selection-active (or (not zhuyin?) zhuyin-selection-active)) - (if zhuyin? - [:* [:p {:class (stl/css :selection-hint)} (tr "workspace.options.text-options.zhuyin-selection-hint")] - [:> tjl/ruby-options* props] - [:> tjl/ruby-hidden-option* props]] - [:> tjl/ruby-advanced-options* props]) - [:* - [:p {:class (stl/css :selection-hint)} - (if zhuyin? (tr "workspace.options.text-options.zhuyin-selection-hint") (tr "workspace.options.text-options.pinyin-selection-hint"))] - (when-not text-selection-active - (if zhuyin? - [:> tjl/ruby-hidden-option* - (mf/spread-props props {:values ruby-values :on-change on-ruby-presentation-change})] - [:> tjl/ruby-presentation-options* - (mf/spread-props props {:values ruby-values - :on-change on-ruby-presentation-change})]))])])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Language panel +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (mf/defc distribute-option* "Distributed alignment (均等割り付け, 分散对齐, 배분 정렬): spreads the @@ -104,44 +300,31 @@ (fn [checked?] (on-change {:text-align (if checked? "distribute" "left")}) (when (some? on-blur) (on-blur))))] - [:div {:class (stl/css :distribute-option)} - [:span {:class (stl/css :option-label)} label] + [:> tcc/option-row* {:label label} [:> switch* {:default-checked (= "distribute" (radio-selected (:text-align values))) :aria-label label :on-change handle-change}]])) (mf/defc language-layout-options* - "Options of the CJK `language` (`:japanese`, `:chinese` or `:korean`)." - [{:keys [language values ruby-values text-selection-active reading-selection-active zhuyin-selection-active - on-change on-ruby-presentation-change on-blur]}] - (let [props (mf/props {:values values :on-change on-change :on-blur on-blur})] - [:div {:class (stl/css :language-layout-options)} - ;; Distribute goes below the writing mode row. - (if (= :japanese language) - [:> tjl/japanese-layout-options* - (mf/spread-props props {:ruby-values ruby-values - :text-selection-active text-selection-active - :on-ruby-presentation-change on-ruby-presentation-change}) - [:> distribute-option* props]] - [:* - [:> tjl/writing-mode-controls* props] - [:> distribute-option* props]]) - (when (= :chinese language) - [:> chinese-decoration-options* props]) - (when (= :chinese language) - [:> chinese-reading-options* - (mf/spread-props props {:ruby-values ruby-values - :text-selection-active text-selection-active - :reading-selection-active reading-selection-active - :zhuyin-selection-active zhuyin-selection-active - :on-ruby-presentation-change on-ruby-presentation-change})]) - (when (= :korean language) - [:> tjl/span-select-option* - (mf/spread-props props - {:attr :korean-wrap - :default-value "syllable" - :label (tr "workspace.options.text-options.korean-wrap") - :options (korean-wrap-options (:korean-wrap values) tr)})])])) + "Options of the CJK `language` (`:japanese`, `:chinese` or `:korean`): + the writing mode row and Distribute, which all share, then the options + of the language." + [{:keys [language values text-selection-active on-change on-blur] :as props}] + [:div {:class (stl/css :language-layout-options)} + [:> tcc/writing-mode-controls* {:values values :on-change on-change :on-blur on-blur} + (when (= :japanese language) + [:> japanese-writing-mode-options* {:values values + :on-change on-change + :on-blur on-blur + :text-selection-active text-selection-active}])] + [:> distribute-option* props] + (case language + :japanese [:> japanese-layout-options* props] + :chinese [:* + [:> chinese-decoration-options* props] + [:> chinese-reading-options* props]] + :korean [:> korean-layout-options* props] + nil)]) (mf/defc language-row* "The language of the texts, opening the language dropdown, and the button diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss index 27d504740f..144e960495 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.scss @@ -90,34 +90,6 @@ padding-block: var(--sp-xs); } -.reading-format-option { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); - align-items: center; - gap: var(--sp-s); -} - -.distribute-option { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--sp-s); - min-block-size: $sz-32; -} - -.option-label { - @include t.use-typography("body-small"); - - color: var(--color-foreground-secondary); -} - -.pinyin-options, -.decoration-options { - display: flex; - flex-direction: column; - gap: var(--sp-xs); -} - .selection-hint { @include t.use-typography("body-small"); diff --git a/frontend/src/app/plugins/text.cljs b/frontend/src/app/plugins/text.cljs index 3b405dc4b3..49795648ac 100644 --- a/frontend/src/app/plugins/text.cljs +++ b/frontend/src/app/plugins/text.cljs @@ -92,15 +92,15 @@ (recur (when continue? (rest styles)) taking? to result)) result)))) -(def ^:private japanese-range-defaults +(def ^:private cjk-range-defaults ;; A range without ruby reports null to plugins. (assoc cjk/span-attr-defaults :ruby nil)) -(defn- shape-japanese-value - "Value of a Japanese span `attr` over a whole text shape proxy, with unset +(defn- shape-cjk-value + "Value of a CJK span `attr` over a whole text shape proxy, with unset spans at their default: 'mixed' when spans differ." [shape-proxy attr] - (let [default (get japanese-range-defaults attr) + (let [default (get cjk-range-defaults attr) values (->> (-> shape-proxy u/proxy->shape :content) (txt/node-seq txt/is-text-node?) (map #(get % attr default)))] @@ -112,16 +112,16 @@ [shape-proxy attr] (-> shape-proxy u/proxy->shape :content (cjk/whole-shape-attr attr) (d/nilv (cjk/enum-default attr)))) -(defn- range-japanese-value - "Value of a Japanese span `attr` over a text range proxy's characters." +(defn- range-cjk-value + "Value of a CJK span `attr` over a text range proxy's characters." [range-proxy start end attr] - (let [default (get japanese-range-defaults attr)] + (let [default (get cjk-range-defaults attr)] (->> (-> range-proxy u/proxy->shape :content (content-range->text+styles start end)) (map #(get % attr default)) (u/mixed-value)))) (defn- enum-value? - "Validator accepting the values of the Japanese layout enum `attr`." + "Validator accepting the values of the CJK layout enum `attr`." [attr] (fn [value] (cjk/valid-enum-value? attr value))) @@ -477,72 +477,72 @@ :fontFeatures {:this true - :get (fn [self] (range-japanese-value self start end :font-features)) + :get (fn [self] (range-cjk-value self start end :font-features)) :set (range-attr-setter plugin-id page-id id start end :fontFeatures :font-features (enum-value? :font-features))} :textCombineUpright {:this true - :get (fn [self] (range-japanese-value self start end :text-combine-upright)) + :get (fn [self] (range-cjk-value self start end :text-combine-upright)) :set (range-attr-setter plugin-id page-id id start end :textCombineUpright :text-combine-upright (enum-value? :text-combine-upright))} :textEmphasis {:this true - :get (fn [self] (range-japanese-value self start end :text-emphasis)) + :get (fn [self] (range-cjk-value self start end :text-emphasis)) :set (range-attr-setter plugin-id page-id id start end :textEmphasis :text-emphasis (enum-value? :text-emphasis))} :textEmphasisPosition {:this true - :get (fn [self] (range-japanese-value self start end :text-emphasis-position)) + :get (fn [self] (range-cjk-value self start end :text-emphasis-position)) :set (range-attr-setter plugin-id page-id id start end :textEmphasisPosition :text-emphasis-position (enum-value? :text-emphasis-position))} :textUnderlinePosition {:this true - :get (fn [self] (range-japanese-value self start end :text-underline-position)) + :get (fn [self] (range-cjk-value self start end :text-underline-position)) :set (range-attr-setter plugin-id page-id id start end :textUnderlinePosition :text-underline-position (enum-value? :text-underline-position))} :warichu {:this true - :get (fn [self] (range-japanese-value self start end :warichu)) + :get (fn [self] (range-cjk-value self start end :warichu)) :set (range-annotation-setter plugin-id page-id id start end :warichu :warichu (enum-value? :warichu))} :annotationClearance {:this true - :get (fn [self] (range-japanese-value self start end :annotation-clearance)) + :get (fn [self] (range-cjk-value self start end :annotation-clearance)) :set (range-attr-setter plugin-id page-id id start end :annotationClearance :annotation-clearance (enum-value? :annotation-clearance))} :ruby {:this true - :get (fn [self] (range-japanese-value self start end :ruby)) + :get (fn [self] (range-cjk-value self start end :ruby)) :set (range-annotation-setter plugin-id page-id id start end :ruby :ruby optional-string?)} :rubyHidden {:this true - :get (fn [self] (range-japanese-value self start end :ruby-hidden)) + :get (fn [self] (range-cjk-value self start end :ruby-hidden)) :set (range-attr-setter plugin-id page-id id start end :rubyHidden :ruby-hidden boolean?)} :rubyZhuyin {:this true - :get (fn [self] (range-japanese-value self start end :ruby-zhuyin)) + :get (fn [self] (range-cjk-value self start end :ruby-zhuyin)) :set (range-attr-setter plugin-id page-id id start end :rubyZhuyin :ruby-zhuyin boolean?)} :rubySize {:this true - :get (fn [self] (range-japanese-value self start end :ruby-size)) + :get (fn [self] (range-cjk-value self start end :ruby-size)) :set (range-attr-setter plugin-id page-id id start end :rubySize :ruby-size (enum-value? :ruby-size))} :rubyAlign {:this true - :get (fn [self] (range-japanese-value self start end :ruby-align)) + :get (fn [self] (range-cjk-value self start end :ruby-align)) :set (range-attr-setter plugin-id page-id id start end :rubyAlign :ruby-align (enum-value? :ruby-align))} :rubyOverhang {:this true - :get (fn [self] (range-japanese-value self start end :ruby-overhang)) + :get (fn [self] (range-cjk-value self start end :ruby-overhang)) :set (range-attr-setter plugin-id page-id id start end :rubyOverhang :ruby-overhang (enum-value? :ruby-overhang))} :rubySide {:this true - :get (fn [self] (range-japanese-value self start end :ruby-side)) + :get (fn [self] (range-cjk-value self start end :ruby-side)) :set (range-attr-setter plugin-id page-id id start end :rubySide :ruby-side (enum-value? :ruby-side))} :lang @@ -978,59 +978,59 @@ :set (shape-attr-setter plugin-id page-id :textOrientation :text-orientation (enum-value? :text-orientation))} {:name "textCombineUpright" - :get #(shape-japanese-value % :text-combine-upright) + :get #(shape-cjk-value % :text-combine-upright) :set (shape-attr-setter plugin-id page-id :textCombineUpright :text-combine-upright (enum-value? :text-combine-upright))} {:name "textEmphasis" - :get #(shape-japanese-value % :text-emphasis) + :get #(shape-cjk-value % :text-emphasis) :set (shape-attr-setter plugin-id page-id :textEmphasis :text-emphasis (enum-value? :text-emphasis))} {:name "textEmphasisPosition" - :get #(shape-japanese-value % :text-emphasis-position) + :get #(shape-cjk-value % :text-emphasis-position) :set (shape-attr-setter plugin-id page-id :textEmphasisPosition :text-emphasis-position (enum-value? :text-emphasis-position))} {:name "textUnderlinePosition" - :get #(shape-japanese-value % :text-underline-position) + :get #(shape-cjk-value % :text-underline-position) :set (shape-attr-setter plugin-id page-id :textUnderlinePosition :text-underline-position (enum-value? :text-underline-position))} {:name "warichu" - :get #(shape-japanese-value % :warichu) + :get #(shape-cjk-value % :warichu) :set (shape-annotation-setter plugin-id page-id :warichu :warichu (enum-value? :warichu))} {:name "fontFeatures" - :get #(shape-japanese-value % :font-features) + :get #(shape-cjk-value % :font-features) :set (shape-attr-setter plugin-id page-id :fontFeatures :font-features (enum-value? :font-features))} {:name "annotationClearance" - :get #(shape-japanese-value % :annotation-clearance) + :get #(shape-cjk-value % :annotation-clearance) :set (shape-attr-setter plugin-id page-id :annotationClearance :annotation-clearance (enum-value? :annotation-clearance))} {:name "ruby" - :get #(shape-japanese-value % :ruby) + :get #(shape-cjk-value % :ruby) :set (shape-annotation-setter plugin-id page-id :ruby :ruby optional-string?)} {:name "rubyHidden" - :get #(shape-japanese-value % :ruby-hidden) + :get #(shape-cjk-value % :ruby-hidden) :set (shape-attr-setter plugin-id page-id :rubyHidden :ruby-hidden boolean?)} {:name "rubyZhuyin" - :get #(shape-japanese-value % :ruby-zhuyin) + :get #(shape-cjk-value % :ruby-zhuyin) :set (shape-attr-setter plugin-id page-id :rubyZhuyin :ruby-zhuyin boolean?)} {:name "rubySize" - :get #(shape-japanese-value % :ruby-size) + :get #(shape-cjk-value % :ruby-size) :set (shape-attr-setter plugin-id page-id :rubySize :ruby-size (enum-value? :ruby-size))} {:name "rubyAlign" - :get #(shape-japanese-value % :ruby-align) + :get #(shape-cjk-value % :ruby-align) :set (shape-attr-setter plugin-id page-id :rubyAlign :ruby-align (enum-value? :ruby-align))} {:name "rubyOverhang" - :get #(shape-japanese-value % :ruby-overhang) + :get #(shape-cjk-value % :ruby-overhang) :set (shape-attr-setter plugin-id page-id :rubyOverhang :ruby-overhang (enum-value? :ruby-overhang))} {:name "rubySide" - :get #(shape-japanese-value % :ruby-side) + :get #(shape-cjk-value % :ruby-side) :set (shape-attr-setter plugin-id page-id :rubySide :ruby-side (enum-value? :ruby-side))} {:name "lang" diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 9d48f43e0d..894db8c4dd 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -431,7 +431,7 @@ (when (= ev TEXT_EDITOR_EVENT_STYLES_CHANGED) (let [current-styles (merge (text-editor/text-editor-get-current-styles) - (text-editor/text-editor-get-current-japanese-styles)) + (text-editor/text-editor-get-current-cjk-styles)) shape-id (text-editor/text-editor-get-active-shape-id)] ;; Keep the caret color matching the text at the caret position. (text-editor/text-editor-apply-caret-color (:fills current-styles)) diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 34fe6cc99c..d0e1be5cef 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -823,8 +823,8 @@ [para] (apply + (map (fn [span] (count (:text span))) (:children para)))) -(defn- span-japanese-styles - "Japanese span attributes with their defaults filled in. Every selection +(defn- span-cjk-styles + "CJK span attributes with their defaults filled in. Every selection snapshot carries all of them, so moving onto an unstyled span resets the controls." [span] @@ -903,8 +903,8 @@ text (apply str (map :text (:children paragraph)))] (cjk/zhuyin-base? (subs text start-offset end-offset))))) -(defn selection-japanese-styles - "Japanese span styles of a WASM selection, read from the cached content. +(defn selection-cjk-styles + "CJK span styles of a WASM selection, read from the cached content. A range with differing values reports :multiple; a caret reports its span's style." [content selection] @@ -922,12 +922,12 @@ (selected-spans paragraphs sel-range))] (when (seq spans) (reduce (fn [result span] - (merge-selection-styles result (span-japanese-styles span))) + (merge-selection-styles result (span-cjk-styles span))) {} spans))))) (defn selection-paragraph-styles - "Profile attrs of the selected paragraphs, including unset and mixed values." + "Language attrs of the selected paragraphs, including unset and mixed values." [content selection] (when (and content selection) (let [{:keys [start-para end-para]} (normalize-selection selection) @@ -943,18 +943,18 @@ :multiple value)))) result - [:text-language :korean-wrap])) + cjk/language-paragraph-attrs)) {} (subvec paragraphs start-para (inc end-para)))))) -(defn text-editor-get-current-japanese-styles - "Japanese span styles and paragraph profiles of the active editor selection." +(defn text-editor-get-current-cjk-styles + "CJK span styles and paragraph language attrs of the active editor selection." [] (when (wasm/ready?) (let [shape-id (text-editor-get-active-shape-id) selection (text-editor-get-selection) content (get-cached-content shape-id)] - (merge (selection-japanese-styles content selection) + (merge (selection-cjk-styles content selection) (selection-paragraph-styles content selection))))) (defn- annotation-style-range diff --git a/frontend/src/app/util/text/layout.cljs b/frontend/src/app/util/text/layout.cljs index be5cda802b..1c515bd03f 100644 --- a/frontend/src/app/util/text/layout.cljs +++ b/frontend/src/app/util/text/layout.cljs @@ -34,39 +34,50 @@ :multiple (first languages))))) +(def ^:private language-groups + "Languages a text can take, grouped as the language dropdown lists them: + `[tag label-key]` pairs per group." + [{:id "japanese" + :label "workspace.options.text-options.language-group-japanese" + :languages [["ja" "workspace.options.text-options.language-japanese"]]} + {:id "korean" + :label "workspace.options.text-options.language-group-korean" + :languages [["ko" "workspace.options.text-options.language-korean"]]} + {:id "chinese" + :label "workspace.options.text-options.language-group-chinese" + ;; Variant names, shown as "Chinese · " outside the group. + :variants true + :languages [["zh-Hans-CN" "workspace.options.text-options.language-chinese-mainland-simplified"] + ["zh-Hant-CN" "workspace.options.text-options.language-chinese-mainland-traditional"] + ["zh-Hant-TW" "workspace.options.text-options.language-chinese-taiwan"] + ["zh-Hant-HK" "workspace.options.text-options.language-chinese-hong-kong"]]}]) + +(def ^:private language-entries + (into {} + (for [{:keys [variants languages]} language-groups + [tag label] languages] + [tag {:label label :variant variants}]))) + (defn language-label "Name of the language `tag` in the Language panel row: \"Mixed\" for `:multiple`, the tag itself when it is not one of the listed languages." [tag] - (case tag - :multiple (tr "settings.multiple") - "ja" (tr "workspace.options.text-options.profile-japanese") - "ko" (tr "workspace.options.text-options.profile-korean") - "zh-Hans-CN" (tr "workspace.options.text-options.language-chinese-variant" - (tr "workspace.options.text-options.profile-chinese-mainland-simplified")) - "zh-Hant-CN" (tr "workspace.options.text-options.language-chinese-variant" - (tr "workspace.options.text-options.profile-chinese-mainland-traditional")) - "zh-Hant-TW" (tr "workspace.options.text-options.language-chinese-variant" - (tr "workspace.options.text-options.profile-chinese-taiwan")) - "zh-Hant-HK" (tr "workspace.options.text-options.language-chinese-variant" - (tr "workspace.options.text-options.profile-chinese-hong-kong")) - tag)) + (if (= :multiple tag) + (tr "settings.multiple") + (if-let [{:keys [label variant]} (get language-entries tag)] + (if variant + (tr "workspace.options.text-options.language-chinese-variant" (tr label)) + (tr label)) + tag))) (defn language-options "Rows of the language dropdown: one group per language, the `selected` tag checked." [selected] - (let [option (fn [tag label] - {:id tag :label label :selected (= selected tag)})] - [{:id "japanese" :header true - :label (tr "workspace.options.text-options.language-group-japanese")} - (option "ja" (tr "workspace.options.text-options.profile-japanese")) - {:id "korean" :header true - :label (tr "workspace.options.text-options.language-group-korean")} - (option "ko" (tr "workspace.options.text-options.profile-korean")) - {:id "chinese" :header true - :label (tr "workspace.options.text-options.language-group-chinese")} - (option "zh-Hans-CN" (tr "workspace.options.text-options.profile-chinese-mainland-simplified")) - (option "zh-Hant-CN" (tr "workspace.options.text-options.profile-chinese-mainland-traditional")) - (option "zh-Hant-TW" (tr "workspace.options.text-options.profile-chinese-taiwan")) - (option "zh-Hant-HK" (tr "workspace.options.text-options.profile-chinese-hong-kong"))])) + (into [] + (mapcat (fn [{:keys [id label languages]}] + (into [{:id id :header true :label (tr label)}] + (map (fn [[tag label]] + {:id tag :label (tr label) :selected (= selected tag)})) + languages))) + language-groups)) diff --git a/frontend/src/app/util/text/writing_mode.cljs b/frontend/src/app/util/text/writing_mode.cljs index dbd2a3a76c..e1c178e4bb 100644 --- a/frontend/src/app/util/text/writing_mode.cljs +++ b/frontend/src/app/util/text/writing_mode.cljs @@ -62,7 +62,7 @@ does not match." [content] (and (not ^boolean (vertical-layout-active?)) - (cjk/vertical-text-content? content))) + (cjk/stored-vertical? content))) (defn renderable-paragraph "Paragraph attrs as the active renderer lays them out: without the vertical @@ -70,7 +70,7 @@ [paragraph] (if ^boolean (vertical-layout-active?) paragraph - (apply dissoc paragraph cjk/whole-shape-paragraph-attrs))) + (apply dissoc paragraph cjk/vertical-writing-attrs))) (defn renderable-content "Text content as the active renderer lays it out (see `active-content` and diff --git a/frontend/test/frontend_tests/render_wasm/texts_test.cljs b/frontend/test/frontend_tests/render_wasm/texts_test.cljs index 29b4590651..f70401ba77 100644 --- a/frontend/test/frontend_tests/render_wasm/texts_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/texts_test.cljs @@ -64,7 +64,7 @@ (t/is (false? (text-editor/reading-selection? content range)))) (t/is (false? (text-editor/reading-selection? nil nil))))) -(t/deftest japanese-styles-follow-the-selected-span +(t/deftest cjk-styles-follow-the-selected-span (let [content (editor-content [{:text "日" :ruby "にち" :text-emphasis "filled-dot"} {:text "本"}])] @@ -82,7 +82,7 @@ :warichu "none" :font-features "none" :annotation-clearance "none"} - (text-editor/selection-japanese-styles + (text-editor/selection-cjk-styles content (selection 0 0 0 1)))) (t/is (= {:text-combine-upright "none" :text-emphasis "none" @@ -98,7 +98,7 @@ :warichu "none" :font-features "none" :annotation-clearance "none"} - (text-editor/selection-japanese-styles + (text-editor/selection-cjk-styles content (selection 0 1 0 2)))))) (t/deftest paragraph-profiles-follow-the-editor-selection @@ -117,7 +117,7 @@ (t/is (= {:text-language :multiple :korean-wrap :multiple} (text-editor/selection-paragraph-styles content (selection 1 1 0 0)))))) -(t/deftest japanese-styles-report-mixed-ranges-and-follow-the-caret +(t/deftest cjk-styles-report-mixed-ranges-and-follow-the-caret (let [content (editor-content [{:text "平成" :text-combine-upright "digits2" @@ -150,16 +150,16 @@ :warichu :multiple :font-features :multiple :annotation-clearance :multiple} - (text-editor/selection-japanese-styles + (text-editor/selection-cjk-styles content (selection 0 0 0 3)))) ;; A caret at the boundary belongs to the preceding span, as in WASM. (t/is (= "digits2" (:text-combine-upright - (text-editor/selection-japanese-styles + (text-editor/selection-cjk-styles content (selection 0 2 0 2))))) (t/is (= "warichu" (:warichu - (text-editor/selection-japanese-styles + (text-editor/selection-cjk-styles content (selection 0 3 0 3))))))) (t/deftest japanese-selection-offsets-are-utf16-code-units @@ -168,7 +168,7 @@ {:text "日" :ruby "にち"}])] (t/is (= "にち" (:ruby - (text-editor/selection-japanese-styles + (text-editor/selection-cjk-styles content (selection 0 2 0 3))))))) (t/deftest editor-history-restores-styled-content-and-selection @@ -545,7 +545,7 @@ (@#'v3-editor/follow-ime-caret! wrapper anchor) (t/is (= "translate(24px, 72px)" (.. wrapper -style -transform)))))))) -(t/deftest whole-shape-paragraph-attrs-apply-to-every-paragraph +(t/deftest vertical-writing-attrs-apply-to-every-paragraph (let [content (-> (editor-content [{:text "一"}] [{:text "二"}] [{:text "三"}]) (assoc-in [:children 0 :children 2 :text-orientation] "upright")) result (text-editor/apply-paragraph-attrs-to-range @@ -776,12 +776,12 @@ (t/is (= #{:japanese} (cfnt/resolve-ambiguous-cjk (langs "東京都渋谷区神南一丁目") nil "ja")))) -(t/deftest wasm-enum-exports-match-the-japanese-value-table +(t/deftest wasm-enum-exports-match-the-cjk-value-table (doseq [[attr values] cjk/enum-values] (let [exported (js->clj (unchecked-get wasm/serializers (name attr)))] (t/is (= values (mapv key (sort-by val exported))) (name attr))))) -(t/deftest unset-or-unknown-japanese-values-serialize-as-the-default +(t/deftest unset-or-unknown-cjk-values-serialize-as-the-default (t/is (= 0 (sr/translate-text-layout-enum :ruby-side nil))) (t/is (= 0 (sr/translate-text-layout-enum :ruby-side ""))) (t/is (= 0 (sr/translate-text-layout-enum :ruby-side "left"))) diff --git a/frontend/test/frontend_tests/ui/settings_text_language_test.cljs b/frontend/test/frontend_tests/ui/settings_text_language_test.cljs index 7fa07dbf07..b700057b46 100644 --- a/frontend/test/frontend_tests/ui/settings_text_language_test.cljs +++ b/frontend/test/frontend_tests/ui/settings_text_language_test.cljs @@ -22,4 +22,4 @@ (t/deftest profile-text-language-shows-the-chosen-language (t/is (str/includes? (render nil) "dashboard.text-language.none")) (t/is (str/includes? (render "zh-Hant-TW") "workspace.options.text-options.language-chinese-variant")) - (t/is (str/includes? (render "ja") "workspace.options.text-options.profile-japanese"))) + (t/is (str/includes? (render "ja") "workspace.options.text-options.language-japanese"))) diff --git a/frontend/test/frontend_tests/ui/text_options_test.cljs b/frontend/test/frontend_tests/ui/text_options_test.cljs index 27a5b8dadf..40dcec2614 100644 --- a/frontend/test/frontend_tests/ui/text_options_test.cljs +++ b/frontend/test/frontend_tests/ui/text_options_test.cljs @@ -16,7 +16,7 @@ [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 :as text-menu] - [app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl] + [app.main.ui.workspace.sidebar.options.menus.text-cjk-controls :as tcc] [app.main.ui.workspace.sidebar.options.menus.text-language-layout :as tll] [app.util.text.layout :as tl] [app.util.text.writing-mode :as wm] @@ -95,7 +95,7 @@ (t/deftest language-row-names-mixed-and-unlisted-languages (t/is (= "settings.multiple" (tl/language-label :multiple))) - (t/is (= "workspace.options.text-options.profile-japanese" (tl/language-label "ja"))) + (t/is (= "workspace.options.text-options.language-japanese" (tl/language-label "ja"))) (t/is (= "zh-Hans" (tl/language-label "zh-Hans")) "a tag outside the list shows as it is")) @@ -119,7 +119,7 @@ (t/deftest file-language-shows-its-options-without-a-remove-button (let [markup (render-language-section "ja" nil)] - (t/is (str/includes? markup "workspace.options.text-options.profile-japanese")) + (t/is (str/includes? markup "workspace.options.text-options.language-japanese")) (t/is (str/includes? markup "workspace.options.text-options.line-adjustment")) (t/is (not (str/includes? markup "data-testid=\"text-language-remove\""))) (t/is (not (str/includes? markup "data-testid=\"text-language-add\""))))) @@ -225,22 +225,22 @@ (t/is (= 3 (count (filter #(str/includes? % "checked=\"\"") inputs))))))) (t/deftest text-emphasis-select-reports-and-restores-canonical-values - (let [options (tjl/text-emphasis-options identity) + (let [options (tcc/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)] + (let [options (tcc/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)))) + (mapv :value (tll/text-combine-upright-options false identity)))) (t/is (= ["none" "all" "digits"] - (mapv :value (tjl/text-combine-upright-options true identity))))) + (mapv :value (tll/text-combine-upright-options true identity))))) (t/deftest ruby-container-styles-preserve-customization (let [style (text-styles/generate-ruby-container-styles @@ -253,8 +253,9 @@ (t/deftest advanced-furigana-options-are-collapsed-by-default (let [markup (rds/renderToStaticMarkup - (mf/element tjl/ruby-presentation-options* + (mf/element tcc/ruby-presentation-options* #js {:values {} + :labels {:advanced "workspace.options.text-options.ruby-advanced-options"} :on-change identity :on-blur identity}))] (t/is (str/includes? markup "data-testid=\"ruby-presentation-options-toggle\"")) @@ -263,7 +264,7 @@ (t/deftest advanced-furigana-options-include-hide-toggle (let [markup (rds/renderToStaticMarkup - (mf/element tjl/ruby-hidden-option* + (mf/element tcc/ruby-hidden-option* #js {:values {:ruby-hidden true} :on-change identity :on-blur identity}))] @@ -311,12 +312,12 @@ (t/is (false? (:ruby-hidden (second spans)))))) (t/deftest mixed-span-values-select-a-disabled-mixed-option - (let [options (tjl/with-mixed-span-option (tjl/text-emphasis-options identity) :multiple)] - (t/is (= "mixed" (tjl/span-select-value :multiple "none"))) - (t/is (= "none" (tjl/span-select-value nil "none"))) + (let [options (tcc/with-mixed-span-option (tcc/text-emphasis-options identity) :multiple)] + (t/is (= "mixed" (tcc/span-select-value :multiple "none"))) + (t/is (= "none" (tcc/span-select-value nil "none"))) (t/is (= "mixed" (:id (last options)))) (t/is (true? (:disabled (last options)))) - (t/is (= options (tjl/with-mixed-span-option options "none")) + (t/is (= options (tcc/with-mixed-span-option options "none")) "an unmixed value adds no entry"))) (t/deftest mixed-radio-values-select-nothing @@ -384,35 +385,35 @@ (t/is (= "digits2" (:text-combine-upright text-values))))) (t/deftest japanese-controls-distinguish-horizontal-and-vertical-modes - (t/is (false? (tjl/vertical-japanese-layout? {}))) - (t/is (false? (tjl/vertical-japanese-layout? + (t/is (false? (tcc/vertical-writing? {}))) + (t/is (false? (tcc/vertical-writing? {:writing-mode "horizontal-tb"}))) - (t/is (true? (tjl/vertical-japanese-layout? + (t/is (true? (tcc/vertical-writing? {:writing-mode "vertical-rl"}))) (t/is (= "palt" - (tjl/proportional-metrics-feature "horizontal-tb"))) + (tll/proportional-metrics-feature "horizontal-tb"))) (t/is (= "palt" - (tjl/proportional-metrics-feature nil))) + (tll/proportional-metrics-feature nil))) (t/is (= "vpal" - (tjl/proportional-metrics-feature "vertical-rl")))) + (tll/proportional-metrics-feature "vertical-rl")))) (t/deftest sidebar-options-offer-the-japanese-enum-values (let [ids #(mapv :id %) values #(mapv :value %)] - (t/is (= (:writing-mode cjk/enum-values) (values (tjl/writing-mode-options identity)))) - (t/is (= (:text-orientation cjk/enum-values) (values (tjl/text-orientation-options identity)))) - (t/is (= (:warichu cjk/enum-values) (values (tjl/warichu-options identity)))) - (t/is (= (:text-emphasis cjk/enum-values) (ids (tjl/text-emphasis-options identity)))) - (t/is (= (:annotation-clearance cjk/enum-values) (ids (tjl/annotation-clearance-options identity)))) - (t/is (= (:ruby-size cjk/enum-values) (ids (tjl/ruby-size-options identity)))) - (t/is (= (:ruby-align cjk/enum-values) (ids (tjl/ruby-align-options identity)))) - (t/is (= (:ruby-overhang cjk/enum-values) (ids (tjl/ruby-overhang-options identity)))) - (t/is (= (:ruby-side cjk/enum-values) (ids (tjl/position-options :ruby-side false)))) - (t/is (= (:ruby-side cjk/enum-values) (ids (tjl/position-options :ruby-side true)))) - (t/is (= (:line-adjustment cjk/enum-values) (ids (tjl/line-adjustment-options identity)))) + (t/is (= (:writing-mode cjk/enum-values) (values (tcc/writing-mode-options identity)))) + (t/is (= (:text-orientation cjk/enum-values) (values (tcc/text-orientation-options identity)))) + (t/is (= (:warichu cjk/enum-values) (values (tll/warichu-options identity)))) + (t/is (= (:text-emphasis cjk/enum-values) (ids (tcc/text-emphasis-options identity)))) + (t/is (= (:annotation-clearance cjk/enum-values) (ids (tcc/annotation-clearance-options identity)))) + (t/is (= (:ruby-size cjk/enum-values) (ids (tcc/ruby-size-options identity)))) + (t/is (= (:ruby-align cjk/enum-values) (ids (tcc/ruby-align-options identity)))) + (t/is (= (:ruby-overhang cjk/enum-values) (ids (tcc/ruby-overhang-options identity)))) + (t/is (= (:ruby-side cjk/enum-values) (ids (tcc/position-options :ruby-side false)))) + (t/is (= (:ruby-side cjk/enum-values) (ids (tcc/position-options :ruby-side true)))) + (t/is (= (:line-adjustment cjk/enum-values) (ids (tll/line-adjustment-options identity)))) (t/is (every? #(cjk/valid-enum-value? :text-combine-upright %) - (values (tjl/text-combine-upright-options true identity)))) - (t/is (every? cjk/digit-combine? (ids (tjl/text-combine-upright-count-options identity)))))) + (values (tll/text-combine-upright-options true identity)))) + (t/is (every? cjk/digit-combine? (ids (tll/text-combine-upright-count-options identity)))))) (t/deftest html-text-drops-vertical-writing-under-a-horizontal-renderer (let [shape {:id (random-uuid) :width 100 :height 100 @@ -557,7 +558,7 @@ (t/deftest japanese-layout-controls-include-line-adjustment - (let [markup (alignment-markup tjl/japanese-layout-options* "vertical-rl" true)] + (let [markup (alignment-markup tll/japanese-layout-options* "vertical-rl" true)] (t/is (str/includes? markup "workspace.options.text-options.line-adjustment")))) @@ -603,35 +604,35 @@ (t/is (not (str/includes? html "