mirror of
https://github.com/penpot/penpot.git
synced 2026-11-06 01:50:39 +00:00
✨ Replace the CJK layout toggles with a text Language panel
The CJK text options were behind per-language "Enable layout" toggles in the file menu and the settings, each with its own sidebar section. A text now has one language, picked in a single Language panel, and the options of that language show below it. - Rename the `vertical-text-layout` flag and feature to `cjk-support`. - Remove the per-file and per-profile layout toggles, the file options that stored them and the `:set-text-layout` change. - Add the Language panel: a grouped dropdown of Japanese, Korean and the regional Chinese variants, a row to change the language and a button to remove it, "Mixed" when the selected texts differ. Picking a language writes it to every paragraph of the texts. - Render the CJK attrs only under the language that uses them, so a text without language is horizontal and plain. The stored content keeps every attr. - Remember the last picked language for the next new texts. - Add File > Language: the language of the texts without one of their own, applied at render time and laid out again when it changes. - Add the profile text language in Settings > Options: new files start with it as their file language. - Extract the grouped dropdown of the board size presets into a shared component used by the presets and the Language panel. - Name the Japanese readings furigana, drop the redundant prefixes of the furigana and pinyin options, move Distribute below the writing mode row and show only the position sides of the writing mode. AI-assisted-by: claude-opus-5-5
This commit is contained in:
parent
9bc8cae3c9
commit
5d647b2cd5
@ -18,6 +18,7 @@
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@ -38,7 +39,7 @@
|
||||
[{:keys [::db/conn] :as cfg}
|
||||
{:keys [id name project-id is-shared revn
|
||||
modified-at deleted-at create-page page-id
|
||||
ignore-sync-until features]
|
||||
ignore-sync-until features text-language]
|
||||
:or {is-shared false revn 0 create-page true}
|
||||
:as params}]
|
||||
|
||||
@ -57,7 +58,10 @@
|
||||
:created-at modified-at
|
||||
:deleted-at deleted-at}
|
||||
{:create-page create-page
|
||||
:page-id page-id})]
|
||||
:page-id page-id})
|
||||
file (cond-> file
|
||||
(some? text-language)
|
||||
(update :data ctf/set-default-text-language text-language))]
|
||||
|
||||
(bfc/insert-file! cfg file)
|
||||
|
||||
@ -102,9 +106,14 @@
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union features))
|
||||
|
||||
;; New files start with the text language the profile picked.
|
||||
language (-> (profile/get-profile conn profile-id)
|
||||
(get-in [:props :default-text-language]))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))]
|
||||
(assoc :features features)
|
||||
(assoc :text-language language))]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.plugins :as ctp]
|
||||
[app.common.types.shape.text :as ctsx]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@ -62,9 +63,8 @@
|
||||
[:map {:title "ProfileProps" :closed true}
|
||||
[:plugins {:optional true} ctp/schema:plugin-registry]
|
||||
[:renderer {:optional true} [::sm/one-of #{:svg :wasm}]]
|
||||
[:japanese-layout-all-files {:optional true} ::sm/boolean]
|
||||
[:chinese-layout-all-files {:optional true} ::sm/boolean]
|
||||
[:korean-layout-all-files {:optional true} ::sm/boolean]
|
||||
;; Text language (BCP 47 tag) the files the profile creates start with.
|
||||
[:default-text-language {:optional true} [:maybe ctsx/schema:text-language]]
|
||||
[:mcp-enabled {:optional true} ::sm/boolean]
|
||||
[:newsletter-updates {:optional true} ::sm/boolean]
|
||||
[:newsletter-news {:optional true} ::sm/boolean]
|
||||
|
||||
@ -145,6 +145,43 @@
|
||||
(let [result (:result out)]
|
||||
(t/is (= 0 (count result))))))))
|
||||
|
||||
(t/deftest new-files-take-the-profile-text-language
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
proj-id (:default-project-id prof)
|
||||
create (fn []
|
||||
(let [out (th/command! {::th/type :create-file
|
||||
::rpc/profile-id (:id prof)
|
||||
:project-id proj-id
|
||||
:name "foobar"})]
|
||||
(t/is (nil? (:error out)))
|
||||
(let [out (th/command! {::th/type :get-file
|
||||
::rpc/profile-id (:id prof)
|
||||
:id (:id (:result out))})]
|
||||
(t/is (nil? (:error out)))
|
||||
(get-in out [:result :data :options :text-language]))))
|
||||
set-language (fn [language]
|
||||
(let [out (th/command! {::th/type :update-profile-props
|
||||
::rpc/profile-id (:id prof)
|
||||
:props {:default-text-language language}})]
|
||||
(t/is (nil? (:error out)))))]
|
||||
|
||||
(t/testing "without a profile language new files have none"
|
||||
(t/is (nil? (create))))
|
||||
|
||||
(t/testing "new files start with the profile language"
|
||||
(set-language "zh-Hant-TW")
|
||||
(t/is (= "zh-Hant-TW" (create))))
|
||||
|
||||
(t/testing "removing the profile language stops applying it"
|
||||
(set-language nil)
|
||||
(t/is (nil? (create))))
|
||||
|
||||
(t/testing "the profile language must be a language tag"
|
||||
(let [out (th/command! {::th/type :update-profile-props
|
||||
::rpc/profile-id (:id prof)
|
||||
:props {:default-text-language "not a tag!"}})]
|
||||
(t/is (th/ex-of-type? (:error out) :validation))))))
|
||||
|
||||
(t/deftest file-gc-with-fragments
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
|
||||
@ -164,44 +164,6 @@
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (nil? (:photo-id (:result out))))))))
|
||||
|
||||
(t/deftest update-profile-japanese-layout-setting
|
||||
(let [profile (th/create-profile* 1)
|
||||
params {::th/type :update-profile-props
|
||||
::rpc/profile-id (:id profile)
|
||||
:props {:japanese-layout-all-files true}}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (true? (get-in out [:result :japanese-layout-all-files])))
|
||||
|
||||
(let [params {::th/type :get-profile
|
||||
::rpc/profile-id (:id profile)}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (true? (get-in out [:result :props :japanese-layout-all-files]))))))
|
||||
|
||||
(t/deftest update-profile-language-layout-settings
|
||||
(let [profile (th/create-profile* 1)
|
||||
props {:japanese-layout-all-files true
|
||||
:chinese-layout-all-files true
|
||||
:korean-layout-all-files false}
|
||||
params {::th/type :update-profile-props
|
||||
::rpc/profile-id (:id profile)
|
||||
:props props}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out)))
|
||||
(t/is (= props (select-keys (:result out) (keys props))))
|
||||
(let [out (th/command! {::th/type :get-profile
|
||||
::rpc/profile-id (:id profile)})]
|
||||
(t/is (= props (select-keys (get-in out [:result :props]) (keys props)))))
|
||||
(let [out (th/command! (assoc params :props {:chinese-layout-all-files false}))]
|
||||
(t/is (nil? (:error out))))
|
||||
(let [out (th/command! {::th/type :get-profile
|
||||
::rpc/profile-id (:id profile)})]
|
||||
(t/is (= {:japanese-layout-all-files true
|
||||
:chinese-layout-all-files false
|
||||
:korean-layout-all-files false}
|
||||
(select-keys (get-in out [:result :props]) (keys props)))))))
|
||||
|
||||
(t/deftest profile-deletion-1
|
||||
(let [prof (th/create-profile* 1)
|
||||
file (th/create-file* 1 {:profile-id (:id prof)
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
"text-editor/v2"
|
||||
"text-editor-wasm/v1"
|
||||
"text-editor-wasm/v1-html-paste"
|
||||
"vertical-text-layout"
|
||||
"cjk-support"
|
||||
"render-wasm/v1"
|
||||
"variants/v1"})
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
"text-editor-wasm/v1"
|
||||
"text-editor-wasm/v1-html-paste"
|
||||
"tokens/numeric-input"
|
||||
"vertical-text-layout"
|
||||
"cjk-support"
|
||||
"render-wasm/v1"})
|
||||
|
||||
;; Features that are mainly backend only or there are a proper
|
||||
@ -136,7 +136,7 @@
|
||||
:feature-text-editor-v2-html-paste "text-editor/v2-html-paste"
|
||||
:feature-text-editor-wasm "text-editor-wasm/v1"
|
||||
:feature-text-editor-wasm-html-paste "text-editor-wasm/v1-html-paste"
|
||||
:vertical-text-layout "vertical-text-layout"
|
||||
:cjk-support "cjk-support"
|
||||
:feature-render-wasm "render-wasm/v1"
|
||||
:feature-variants "variants/v1"
|
||||
:feature-token-input "tokens/numeric-input"
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.text :as ctsx]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.tokens-status :as ctos]
|
||||
@ -435,6 +436,11 @@
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
|
||||
[:set-default-text-language
|
||||
[:map {:title "SetDefaultTextLanguage"}
|
||||
[:type [:= :set-default-text-language]]
|
||||
[:language [:maybe ctsx/schema:text-language]]]]
|
||||
|
||||
[:set-base-font-size
|
||||
[:map {:title "ModBaseFontSize"}
|
||||
[:type [:= :set-base-font-size]]
|
||||
@ -444,13 +450,7 @@
|
||||
[:map {:title "SetTokensSource"}
|
||||
[:type [:= :set-tokens-source]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:library-id [:maybe ::sm/uuid]]]]
|
||||
|
||||
[:set-text-layout
|
||||
[:map {:title "SetTextLayout"}
|
||||
[:type [:= :set-text-layout]]
|
||||
[:language [:enum :japanese :chinese :korean]]
|
||||
[:enabled ::sm/boolean]]]])
|
||||
[:library-id [:maybe ::sm/uuid]]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
@ -1159,6 +1159,10 @@
|
||||
|
||||
;; === Design Tokens configuration
|
||||
|
||||
(defmethod process-change :set-default-text-language
|
||||
[data {:keys [language]}]
|
||||
(ctf/set-default-text-language data language))
|
||||
|
||||
(defmethod process-change :set-base-font-size
|
||||
[data {:keys [base-font-size]}]
|
||||
(ctf/set-base-font-size data base-font-size))
|
||||
@ -1167,10 +1171,6 @@
|
||||
[data {:keys [library-id]}]
|
||||
(cfo/set-tokens-source data library-id))
|
||||
|
||||
(defmethod process-change :set-text-layout
|
||||
[data {:keys [language enabled]}]
|
||||
(ctf/set-text-layout-enabled data language enabled))
|
||||
|
||||
;; === Operations
|
||||
|
||||
(def decode-shape-attrs
|
||||
|
||||
@ -1153,6 +1153,18 @@
|
||||
:base-font-size previous-font-size})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn set-default-text-language
|
||||
[changes language]
|
||||
(assert-file-data! changes)
|
||||
(let [file-data (::file-data (meta changes))
|
||||
previous (ctf/get-default-text-language file-data)]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-default-text-language
|
||||
:language language})
|
||||
(update :undo-changes conj {:type :set-default-text-language
|
||||
:language previous})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn set-tokens-source
|
||||
[changes library-id]
|
||||
(assert-library! changes)
|
||||
@ -1168,20 +1180,6 @@
|
||||
:library-id prev-val})
|
||||
(apply-changes-local))))
|
||||
|
||||
(defn set-text-layout
|
||||
[changes language enabled?]
|
||||
(assert-file-data! changes)
|
||||
(let [file-data (::file-data (meta changes))
|
||||
previous-enabled (ctf/text-layout-enabled? file-data language)]
|
||||
(-> changes
|
||||
(update :redo-changes conj {:type :set-text-layout
|
||||
:language language
|
||||
:enabled enabled?})
|
||||
(update :undo-changes conj {:type :set-text-layout
|
||||
:language language
|
||||
:enabled previous-enabled})
|
||||
(apply-changes-local))))
|
||||
|
||||
;; Misc changes
|
||||
|
||||
(defn reorder-children
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
;; Show WASM renderer info label (hidden by default).
|
||||
:render-wasm-info
|
||||
:render-switch
|
||||
:vertical-text-layout
|
||||
:cjk-support
|
||||
:hide-release-modal
|
||||
:subscriptions
|
||||
:inspect-styles
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
[app.common.types.plugins :refer [schema:plugin-data]]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.text :as ctsx]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.tokens-status :as ctos]
|
||||
@ -78,9 +79,9 @@
|
||||
[:map {:title "FileOptions"}
|
||||
[:components-v2 {:optional true} ::sm/boolean]
|
||||
[:base-font-size {:optional true} :string]
|
||||
[:japanese-layout {:optional true} ::sm/boolean]
|
||||
[:chinese-layout {:optional true} ::sm/boolean]
|
||||
[:korean-layout {:optional true} ::sm/boolean]])
|
||||
;; Language of the texts that have none of their own (see
|
||||
;; `get-default-text-language`).
|
||||
[:text-language {:optional true} ctsx/schema:text-language]])
|
||||
|
||||
(def schema:data
|
||||
[:map {:title "FileData"}
|
||||
@ -1265,33 +1266,20 @@
|
||||
[file-data base-font-size]
|
||||
(assoc-in file-data [:options :base-font-size] base-font-size))
|
||||
|
||||
;; Default text language
|
||||
|
||||
;; Text layout controls
|
||||
(defn get-default-text-language
|
||||
"Language (a BCP 47 tag) of the texts of the file that have none of their
|
||||
own, or nil."
|
||||
[file-data]
|
||||
(get-in file-data [:options :text-language]))
|
||||
|
||||
(defn text-layout-option
|
||||
[language]
|
||||
(case language
|
||||
:japanese :japanese-layout
|
||||
:chinese :chinese-layout
|
||||
:korean :korean-layout))
|
||||
|
||||
(defn text-layout-preference
|
||||
[language]
|
||||
(case language
|
||||
:japanese :japanese-layout-all-files
|
||||
:chinese :chinese-layout-all-files
|
||||
:korean :korean-layout-all-files))
|
||||
|
||||
(defn text-layout-enabled?
|
||||
(defn set-default-text-language
|
||||
"Set the file text language; nil removes it."
|
||||
[file-data language]
|
||||
(true? (get-in file-data [:options (text-layout-option language)])))
|
||||
|
||||
(defn set-text-layout-enabled
|
||||
[file-data language enabled?]
|
||||
(let [option (text-layout-option language)]
|
||||
(if enabled?
|
||||
(assoc-in file-data [:options option] true)
|
||||
(d/update-in-when file-data [:options] dissoc option))))
|
||||
(if (some? language)
|
||||
(assoc-in file-data [:options :text-language] language)
|
||||
(d/update-in-when file-data [:options] dissoc :text-language)))
|
||||
|
||||
|
||||
;; Ref Chains
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.types.text.cjk-layout
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@ -53,6 +54,11 @@
|
||||
(def whole-shape-paragraph-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))
|
||||
|
||||
;; Ruby attrs that change how a reading looks, not the reading itself.
|
||||
(def ruby-presentation-attrs
|
||||
[:ruby-hidden :ruby-zhuyin :ruby-size :ruby-align :ruby-overhang :ruby-side])
|
||||
@ -219,3 +225,80 @@
|
||||
"True when the text content flows vertically (vertical-rl)."
|
||||
[content]
|
||||
(= "vertical-rl" (content-writing-mode content)))
|
||||
|
||||
;; --- Active language
|
||||
|
||||
(defn language-family
|
||||
"CJK language of a BCP 47 `tag` (`:japanese`, `:chinese` or `:korean`),
|
||||
or nil for any other value."
|
||||
[tag]
|
||||
(when (string? tag)
|
||||
(case (-> tag (str/split "-") first str/lower)
|
||||
"ja" :japanese
|
||||
"zh" :chinese
|
||||
"ko" :korean
|
||||
nil)))
|
||||
|
||||
;; Span 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 #{}})
|
||||
|
||||
(defn content-language
|
||||
"Language of a text content: the first paragraph's `:text-language`, as a
|
||||
text has one language. Nil when unset."
|
||||
[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- 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)))
|
||||
(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))))
|
||||
|
||||
(defn active-content
|
||||
"Text `content` as the renderer lays it out. A paragraph without language
|
||||
takes `default-language` (the file language), and the CJK attrs its
|
||||
language does not use are left out: without a CJK language a text is
|
||||
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))))
|
||||
|
||||
(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)))
|
||||
|
||||
@ -252,39 +252,22 @@
|
||||
"The parent must use the updated bounds of both children")
|
||||
(assert-idempotent data changes))))))
|
||||
|
||||
(t/deftest language-layout-changes-are-independent
|
||||
(t/deftest default-text-language-round-trips-through-undo
|
||||
(let [data (assoc (make-file-data (uuid/custom 2 2) (uuid/custom 1 1))
|
||||
:options {:japanese-layout true :base-font-size "16"})]
|
||||
(doseq [language [:japanese :chinese :korean]]
|
||||
(let [change {:type :set-text-layout :language language :enabled true}
|
||||
enabled (ch/process-changes data [change])
|
||||
disabled (ch/process-changes enabled [(assoc change :enabled false)])
|
||||
option (case language
|
||||
:japanese :japanese-layout
|
||||
:chinese :chinese-layout
|
||||
:korean :korean-layout)]
|
||||
(t/is (ch/valid-change? change))
|
||||
(t/is (true? (get-in enabled [:options option])))
|
||||
(t/is (not (contains? (:options disabled) option)))
|
||||
(t/is (= (dissoc (:options data) option) (:options disabled)))
|
||||
(t/is (= (dissoc data :options) (dissoc enabled :options))))))
|
||||
(t/is (not (ch/valid-change? {:type :set-text-layout :language :english
|
||||
:enabled true}))))
|
||||
|
||||
(t/deftest language-layout-switches-round-trip-through-undo
|
||||
(let [data (assoc (make-file-data (uuid/custom 2 2) (uuid/custom 1 1))
|
||||
:options {:japanese-layout true :base-font-size "16"})]
|
||||
(doseq [language [:japanese :chinese :korean]
|
||||
enabled? [true false]]
|
||||
(let [changes (-> (pcb/empty-changes)
|
||||
(pcb/with-file-data data)
|
||||
(pcb/set-text-layout language enabled?))
|
||||
edited (ch/process-changes data (:redo-changes changes))
|
||||
:options {:base-font-size "16"})]
|
||||
(doseq [[before after] [[nil "ja"] ["ja" "zh-Hant-TW"] ["ko" nil]]]
|
||||
(let [data (ctf/set-default-text-language data before)
|
||||
changes (-> (pcb/empty-changes)
|
||||
(pcb/with-file-data data)
|
||||
(pcb/set-default-text-language after))
|
||||
edited (ch/process-changes data (:redo-changes changes))
|
||||
restored (ch/process-changes edited (:undo-changes changes))]
|
||||
(t/is (= enabled? (ctf/text-layout-enabled? edited language)))
|
||||
(t/is (every? ch/valid-change? (:redo-changes changes)))
|
||||
(t/is (= after (ctf/get-default-text-language edited)))
|
||||
(t/is (= data restored))
|
||||
(t/is (sm/validate ctf/schema:options (json-roundtrip (:options edited))))
|
||||
(t/is (= (:options edited) (json-roundtrip (:options edited))))))))
|
||||
(t/is (= "16" (get-in edited [:options :base-font-size])))
|
||||
(t/is (sm/validate ctf/schema:options (:options edited))))))
|
||||
(t/is (not (ch/valid-change? {:type :set-default-text-language :language "not a tag!"}))))
|
||||
|
||||
(t/deftest add-obj
|
||||
(let [file-id (uuid/custom 2 2)
|
||||
|
||||
@ -564,3 +564,79 @@
|
||||
|
||||
(t/testing "no spans give no spans"
|
||||
(t/is (= [] (cttx/merge-equal-spans [])))))
|
||||
|
||||
(defn- cjk-content
|
||||
"One paragraph with a span carrying the CJK attrs of every language."
|
||||
[paragraph-attrs]
|
||||
{:type "root"
|
||||
:line-adjustment "push-out-first"
|
||||
:children
|
||||
[{:type "paragraph-set"
|
||||
:children
|
||||
[(merge {:type "paragraph"
|
||||
:writing-mode "vertical-rl"
|
||||
:text-orientation "upright"
|
||||
:text-align "distribute"
|
||||
:korean-wrap "keep-words"
|
||||
:children [{:text "日本"
|
||||
:ruby "にほん"
|
||||
:ruby-zhuyin true
|
||||
:text-combine-upright "digits"
|
||||
:text-emphasis-position "over-left"
|
||||
:warichu "warichu"
|
||||
:font-features "palt"}]}
|
||||
paragraph-attrs)]}]})
|
||||
|
||||
(defn- first-paragraph
|
||||
[content]
|
||||
(get-in content [:children 0 :children 0]))
|
||||
|
||||
(t/deftest language-family-follows-the-primary-subtag
|
||||
(t/is (= :japanese (cjk/language-family "ja-JP")))
|
||||
(t/is (= :chinese (cjk/language-family "zh-Hant-TW")))
|
||||
(t/is (= :korean (cjk/language-family "KO-kr")))
|
||||
(t/is (nil? (cjk/language-family "en-GB")))
|
||||
(t/is (nil? (cjk/language-family :multiple)))
|
||||
(t/is (nil? (cjk/language-family nil))))
|
||||
|
||||
(t/deftest text-without-language-renders-horizontal-and-plain
|
||||
(let [content (cjk-content {})
|
||||
active (cjk/active-content content nil)
|
||||
paragraph (first-paragraph active)]
|
||||
(t/is (not (contains? paragraph :writing-mode)))
|
||||
(t/is (not (contains? paragraph :text-orientation)))
|
||||
(t/is (= "left" (:text-align paragraph)))
|
||||
(t/is (not (contains? paragraph :korean-wrap)))
|
||||
(t/is (= {:text "日本"} (first (:children paragraph))))
|
||||
(t/is (not (contains? active :line-adjustment)))
|
||||
(t/is (nil? (cjk/active-writing-mode content nil)))
|
||||
(t/is (= content (cjk-content {})) "the stored content keeps every attr")))
|
||||
|
||||
(t/deftest each-language-renders-only-its-own-attrs
|
||||
(let [span #(first (:children (first-paragraph %)))]
|
||||
(let [active (cjk/active-content (cjk-content {:text-language "ja"}) nil)]
|
||||
(t/is (= {:text "日本" :ruby "にほん" :text-combine-upright "digits"
|
||||
:warichu "warichu" :font-features "palt"}
|
||||
(span active)))
|
||||
(t/is (= "push-out-first" (:line-adjustment active)))
|
||||
(t/is (= "vertical-rl" (:writing-mode (first-paragraph active))))
|
||||
(t/is (= "distribute" (:text-align (first-paragraph active))))
|
||||
(t/is (not (contains? (first-paragraph active) :korean-wrap))))
|
||||
(let [active (cjk/active-content (cjk-content {:text-language "zh-Hant-TW"}) nil)]
|
||||
(t/is (= {:text "日本" :ruby "にほん" :ruby-zhuyin true :text-emphasis-position "over-left"}
|
||||
(span active)))
|
||||
(t/is (not (contains? active :line-adjustment))))
|
||||
(let [active (cjk/active-content (cjk-content {:text-language "ko"}) nil)]
|
||||
(t/is (= {:text "日本"} (span active)))
|
||||
(t/is (= "keep-words" (:korean-wrap (first-paragraph active))))
|
||||
(t/is (= "vertical-rl" (:writing-mode (first-paragraph active)))))))
|
||||
|
||||
(t/deftest text-without-language-takes-the-file-language
|
||||
(let [content (cjk-content {})
|
||||
active (cjk/active-content content "ja")]
|
||||
(t/is (= "ja" (:text-language (first-paragraph active))))
|
||||
(t/is (= "vertical-rl" (:writing-mode (first-paragraph active))))
|
||||
(t/is (= "vertical-rl" (cjk/active-writing-mode content "ja"))))
|
||||
(let [active (cjk/active-content (cjk-content {:text-language "ko"}) "ja")]
|
||||
(t/is (= "ko" (:text-language (first-paragraph active)))
|
||||
"the text language wins over the file language")))
|
||||
|
||||
@ -151,12 +151,19 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; `:add-obj` / `:mov-objects` are not tracked via `*shape-changes*`.
|
||||
;; Emit only for structural commits, after file data is in state.
|
||||
(when (and (not ignore-wasm?)
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(redo-changes-need-wasm-object-sync? redo-changes))
|
||||
(rx/of (sync-wasm-structural-changes {:redo-changes redo-changes}))))))
|
||||
(let [wasm? (and (not ignore-wasm?)
|
||||
(features/active-feature? state "render-wasm/v1"))]
|
||||
(rx/merge
|
||||
;; `:add-obj` / `:mov-objects` are not tracked via `*shape-changes*`.
|
||||
;; Emit only for structural commits, after file data is in state.
|
||||
(if (and wasm? (redo-changes-need-wasm-object-sync? redo-changes))
|
||||
(rx/of (sync-wasm-structural-changes {:redo-changes redo-changes}))
|
||||
(rx/empty))
|
||||
;; The texts without language take the file one, so they render
|
||||
;; differently when it changes (handled by the workspace).
|
||||
(if (and wasm? (some #(= :set-default-text-language (:type %)) redo-changes))
|
||||
(rx/of (ptk/data-event ::default-text-language-changed {}))
|
||||
(rx/empty)))))))
|
||||
|
||||
(defn commit
|
||||
"Create a commit event instance"
|
||||
|
||||
@ -411,7 +411,7 @@
|
||||
(assoc :recent-fonts (:recent-fonts storage/user))
|
||||
(assoc :current-file-id file-id)
|
||||
(assoc :workspace-presence {})
|
||||
(update :workspace-global dissoc :default-font)
|
||||
(update :workspace-global dissoc :default-font :default-text-language)
|
||||
(update :comments-local dcmt/merge-persisted-filters)))
|
||||
|
||||
ptk/WatchEvent
|
||||
@ -523,6 +523,13 @@
|
||||
(rx/map (fn [{:keys [ids]}]
|
||||
(dwwt/resize-wasm-text-all ids {:skip-component-sync? true}))))
|
||||
|
||||
;; Lay out again the texts that take the file language when it
|
||||
;; changes, also on undo, redo and changes from other sessions.
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dch/default-text-language-changed))
|
||||
(rx/filter render-wasm-ready?)
|
||||
(rx/map #(dwwt/refresh-file-language-texts)))
|
||||
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
@ -616,7 +623,7 @@
|
||||
:workspace-tokens
|
||||
:workspace-undo
|
||||
:workspace-versions)
|
||||
(update :workspace-global dissoc :read-only? :preview-id :default-font)
|
||||
(update :workspace-global dissoc :read-only? :preview-id :default-font :default-text-language)
|
||||
(assoc-in [:workspace-global :options-mode] :design)
|
||||
(update :files d/update-vals #(dissoc % :data))))
|
||||
|
||||
@ -678,16 +685,20 @@
|
||||
(dm/export layout/toggle-layout-flag)
|
||||
(dm/export layout/remove-layout-flag)
|
||||
|
||||
(defn set-text-layout
|
||||
[language enabled?]
|
||||
(ptk/reify ::set-text-layout
|
||||
(defn set-default-text-language
|
||||
"Set the language of the file texts that have none of their own; nil
|
||||
removes it. The language remembered for new texts is dropped, so they take
|
||||
the new file language. The workspace lays the texts out again once the
|
||||
change applies (see `initialize-workspace`)."
|
||||
[language]
|
||||
(ptk/reify ::set-default-text-language
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [file-data (dsh/lookup-file-data state)
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/set-text-layout language enabled?))]
|
||||
(rx/of (dch/commit-changes changes))))))
|
||||
(let [changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-file-data (dsh/lookup-file-data state))
|
||||
(pcb/set-default-text-language language))]
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(dwtxt/remember-text-language nil))))))
|
||||
|
||||
;; --- Profile
|
||||
|
||||
|
||||
@ -982,6 +982,17 @@
|
||||
(not multiple?)
|
||||
(update :workspace-global assoc :default-font font))))))
|
||||
|
||||
(defn remember-text-language
|
||||
"Remember `language` for the next new texts of the file; nil forgets it,
|
||||
so they take the file language."
|
||||
[language]
|
||||
(ptk/reify ::remember-text-language
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(if (some? language)
|
||||
(assoc-in state [:workspace-global :default-text-language] language)
|
||||
(update state :workspace-global dissoc :default-text-language)))))
|
||||
|
||||
(defn apply-text-modifier
|
||||
[shape text-modifier]
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.render-wasm.api.select :as wselect]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.text :as txt]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace :as-alias dw]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
@ -276,3 +277,26 @@
|
||||
(rx/empty)
|
||||
resize-stream)))))
|
||||
resize-stream))))))
|
||||
|
||||
(defn- file-language-text?
|
||||
"True when some paragraph of the text `shape` has no language of its own,
|
||||
so it renders with the file language."
|
||||
[shape]
|
||||
(and (cfh/text-shape? shape)
|
||||
(let [paragraphs (txt/node-seq txt/is-paragraph-node? (:content shape))]
|
||||
(or (empty? paragraphs)
|
||||
(some #(nil? (:text-language %)) paragraphs)))))
|
||||
|
||||
(defn refresh-file-language-texts
|
||||
"Lay out again the texts of the page that render with the file language,
|
||||
after it changed, and resize the auto-sized ones."
|
||||
[]
|
||||
(ptk/reify ::refresh-file-language-texts
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [shapes (into [] (filter file-language-text?) (vals (dsh/lookup-page-objects state)))]
|
||||
(rx/concat
|
||||
(rx/defer (fn []
|
||||
(wasm.api/refresh-text-contents shapes)
|
||||
(rx/empty)))
|
||||
(rx/of (resize-wasm-text-all (mapv :id shapes))))))))
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
[app.main.data.workspace.tokens.selected-set :as dwts]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.text.layout :as tl]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.string :as str]
|
||||
[okulary.core :as l]))
|
||||
@ -247,6 +248,10 @@
|
||||
(def default-font
|
||||
(l/derived :default-font workspace-global))
|
||||
|
||||
(def default-text-language
|
||||
"Language the last text option change picked, for the next new texts."
|
||||
(l/derived :default-text-language workspace-global))
|
||||
|
||||
(def inspect-expanded
|
||||
(l/derived :inspect-expanded workspace-local))
|
||||
|
||||
@ -290,22 +295,6 @@
|
||||
"Currently working file data on workspace"
|
||||
(l/derived dsh/lookup-file-data st/state))
|
||||
|
||||
(def text-layout-config
|
||||
"Control availability by language; it never changes paragraph profiles."
|
||||
(l/derived
|
||||
(fn [state]
|
||||
(let [file-data (dsh/lookup-file-data state)
|
||||
props (dm/get-in state [:profile :props])
|
||||
feature? (contains? (:features state) "vertical-text-layout")
|
||||
config (fn [language]
|
||||
{:feature-enabled feature?
|
||||
:file? (ctf/text-layout-enabled? file-data language)
|
||||
:all-files? (true? (get props (ctf/text-layout-preference language)))})]
|
||||
{:japanese (config :japanese)
|
||||
:chinese (config :chinese)
|
||||
:korean (config :korean)}))
|
||||
st/state =))
|
||||
|
||||
(def workspace-file-colors
|
||||
(l/derived (fn [{:keys [id] :as data}]
|
||||
(some-> (:colors data) (update-vals #(assoc % :file-id id))))
|
||||
@ -369,6 +358,39 @@
|
||||
[ids]
|
||||
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects =))
|
||||
|
||||
(def file-text-language
|
||||
"Language of the texts of the open file that have none of their own."
|
||||
(l/derived ctf/get-default-text-language workspace-data))
|
||||
|
||||
(defn texts-language
|
||||
"Language of the texts among `ids` and of the texts inside the groups
|
||||
among them (see `tl/get-texts-language`): `:language` is the one they
|
||||
render with, taking the file language when they have none, and `:own` the
|
||||
one they store."
|
||||
[ids]
|
||||
(l/derived
|
||||
(fn [state]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
contents (into []
|
||||
(mapcat
|
||||
(fn [id]
|
||||
(let [shape (get objects id)]
|
||||
(cond
|
||||
(cph/text-shape? shape)
|
||||
[(:content shape)]
|
||||
|
||||
(cph/group-shape? shape)
|
||||
(into []
|
||||
(comp (map (d/getf objects))
|
||||
(filter cph/text-shape?)
|
||||
(map :content))
|
||||
(cph/get-children-ids objects id))))))
|
||||
ids)]
|
||||
{:language (tl/get-texts-language contents (ctf/get-default-text-language
|
||||
(dsh/lookup-file-data state)))
|
||||
:own (tl/get-texts-language contents)}))
|
||||
st/state =))
|
||||
|
||||
(defn parents-by-ids
|
||||
[ids]
|
||||
(l/derived
|
||||
|
||||
84
frontend/src/app/main/ui/components/grouped_dropdown.cljs
Normal file
84
frontend/src/app/main/ui/components/grouped_dropdown.cljs
Normal file
@ -0,0 +1,84 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.main.ui.components.grouped-dropdown
|
||||
"Dropdown list of options split in groups by non-selectable header rows,
|
||||
with an optional search bar. The caller owns the trigger and the open
|
||||
state, and positions the list from the nearest positioned ancestor."
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.search-bar :refer [search-bar*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc option-row*
|
||||
{::mf/private true}
|
||||
[{:keys [option on-select]}]
|
||||
(let [{:keys [id label detail selected]} option
|
||||
|
||||
handle-click
|
||||
(mf/use-fn
|
||||
(mf/deps id on-select)
|
||||
(fn [event]
|
||||
(on-select id event)))
|
||||
|
||||
handle-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps id on-select)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/prevent-default event)
|
||||
(on-select id event))))]
|
||||
[:li {:class (stl/css-case :option true
|
||||
:selected selected)
|
||||
:role "option"
|
||||
:aria-selected (boolean selected)
|
||||
:tab-index 0
|
||||
:data-testid (str "grouped-dropdown-option-" id)
|
||||
:on-click handle-click
|
||||
:on-key-down handle-key-down}
|
||||
[:div {:class (stl/css :option-text)}
|
||||
[:span {:class (stl/css :option-label)} label]
|
||||
(when (some? detail)
|
||||
[:span {:class (stl/css :option-detail)} detail])]
|
||||
(when selected
|
||||
[:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))
|
||||
|
||||
(mf/defc grouped-dropdown*
|
||||
"`options` holds the rows in display order: a header row is
|
||||
`{:id :label :header true}`, an option row `{:id :label}` with an optional
|
||||
`:detail` text and `:selected` flag. `on-select` receives the option id and
|
||||
the event. Passing `on-search-change` shows the search bar; `empty-label`
|
||||
is the row shown when `options` is empty."
|
||||
[{:keys [show on-close container options on-select class
|
||||
search-term search-placeholder on-search-change empty-label]}]
|
||||
[:& dropdown {:show show
|
||||
:on-close on-close
|
||||
:container container}
|
||||
[:div {:class [class (stl/css :dropdown)]
|
||||
:on-click dom/stop-propagation}
|
||||
(when (some? on-search-change)
|
||||
[:div {:class (stl/css :search)}
|
||||
[:> search-bar* {:on-change on-search-change
|
||||
:value search-term
|
||||
:auto-focus true
|
||||
:placeholder search-placeholder}]])
|
||||
[:ul {:class (stl/css :option-list)
|
||||
:role "listbox"}
|
||||
(if (empty? options)
|
||||
[:li {:class (stl/css :empty)} empty-label]
|
||||
(for [option options]
|
||||
(if (:header option)
|
||||
[:li {:key (:id option)
|
||||
:class (stl/css :header)
|
||||
:role "presentation"}
|
||||
(:label option)]
|
||||
[:> option-row* {:key (:id option)
|
||||
:option option
|
||||
:on-select on-select}])))]]])
|
||||
93
frontend/src/app/main/ui/components/grouped_dropdown.scss
Normal file
93
frontend/src/app/main/ui/components/grouped_dropdown.scss
Normal file
@ -0,0 +1,93 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.dropdown {
|
||||
@extend %dropdown-wrapper;
|
||||
|
||||
margin-top: deprecated.$s-2;
|
||||
max-height: 70vh;
|
||||
width: deprecated.$s-252;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: deprecated.$s-4;
|
||||
border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent);
|
||||
}
|
||||
|
||||
.option-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.header,
|
||||
.empty {
|
||||
@extend %dropdown-element-base;
|
||||
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: var(--menu-foreground-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.option {
|
||||
@extend %dropdown-element-base;
|
||||
|
||||
.option-text {
|
||||
display: flex;
|
||||
gap: deprecated.$s-8;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.option-label,
|
||||
.option-detail {
|
||||
color: var(--menu-foreground-color-rest);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
svg {
|
||||
@extend %button-icon-small;
|
||||
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.option-label {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
|
||||
.option-label {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.settings.options
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.types.file :as ctf]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.notifications :as ntf]
|
||||
@ -17,6 +16,7 @@
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.ds.controls.switch :refer [switch*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
@ -24,6 +24,7 @@
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.text.layout :as tl]
|
||||
[app.util.theme :as theme]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@ -123,46 +124,45 @@
|
||||
:on-change handle-render-change}]]
|
||||
[:> text* {:typography t/body-medium :class (stl/css :feedback)} [:a {:href "#" :on-click go-settings-feedback :class (stl/css :link)} (tr "dashboard.webgl-switch.feedback") [:> icon* {:icon-id "arrow-up-right" :size "s"}]]]]))
|
||||
|
||||
(mf/defc language-layout-setting*
|
||||
[{:keys [language label enabled]}]
|
||||
(let [handle-change
|
||||
(mf/use-fn
|
||||
(mf/deps language)
|
||||
(fn [enabled?]
|
||||
(st/emit! (ev/event {::ev/name (str (if enabled? "enable-" "disable-")
|
||||
(name language) "-layout-all-files")
|
||||
::ev/origin "settings"})
|
||||
(du/update-profile-props
|
||||
{(ctf/text-layout-preference language) enabled?})
|
||||
(ntf/success (tr "notifications.profile-saved")))))]
|
||||
[:> switch* {:label label
|
||||
:default-checked enabled
|
||||
:on-change handle-change}]))
|
||||
(def ^:private no-text-language
|
||||
"Id of the \"None\" choice: the DS select shows \"--\" for a blank id."
|
||||
"none")
|
||||
|
||||
(mf/defc language-layout-settings*
|
||||
[{:keys [config]}]
|
||||
[:section {:class (stl/css :text-layout-container)}
|
||||
[:> heading* {:class (stl/css :title) :level 2 :typography t/title-large}
|
||||
(tr "dashboard.text-layout.title")]
|
||||
[:> text* {:class (stl/css :description) :typography t/body-medium}
|
||||
(tr "dashboard.text-layout.description")]
|
||||
[:div {:class (stl/css :text-layout-form)}
|
||||
[:> language-layout-setting* {:language :japanese
|
||||
:label (tr "dashboard.japanese-layout.enable-all-files")
|
||||
:enabled (get-in config [:japanese :all-files?])}]
|
||||
[:> language-layout-setting* {:language :chinese
|
||||
:label (tr "dashboard.chinese-layout.enable-all-files")
|
||||
:enabled (get-in config [:chinese :all-files?])}]
|
||||
[:> language-layout-setting* {:language :korean
|
||||
:label (tr "dashboard.korean-layout.enable-all-files")
|
||||
:enabled (get-in config [:korean :all-files?])}]]])
|
||||
(mf/defc text-language-settings*
|
||||
"Text language the new files of the profile start with."
|
||||
{::mf/private true}
|
||||
[{:keys [language]}]
|
||||
(let [options
|
||||
(mf/with-memo []
|
||||
(into [{:id no-text-language :label (tr "dashboard.text-language.none")}]
|
||||
(comp (remove :header)
|
||||
(map (fn [{:keys [id]}] {:id id :label (tl/language-label id)})))
|
||||
(tl/language-options nil)))
|
||||
|
||||
handle-change
|
||||
(mf/use-fn
|
||||
(fn [id]
|
||||
(st/emit! (du/update-profile-props
|
||||
{:default-text-language (when (not= no-text-language id) id)})
|
||||
(ntf/success (tr "notifications.profile-saved")))))]
|
||||
|
||||
[:section {:class (stl/css :text-language-container)}
|
||||
[:> heading* {:class (stl/css :title) :level 2 :typography t/title-large}
|
||||
(tr "dashboard.text-language.title")]
|
||||
[:> text* {:class (stl/css :description) :typography t/body-medium}
|
||||
(tr "dashboard.text-language.description")]
|
||||
[:div {:class (stl/css :text-language-form)}
|
||||
[:> select* {:default-selected (or language no-text-language)
|
||||
:aria-label (tr "dashboard.text-language.title")
|
||||
:options options
|
||||
:on-change handle-change
|
||||
:data-testid "setting-text-language"}]]]))
|
||||
|
||||
(mf/defc options-page*
|
||||
[]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
renderer (or (-> profile :props :renderer) :svg)
|
||||
layout-config (mf/deref refs/text-layout-config)
|
||||
layout-feature? (features/use-feature "vertical-text-layout")]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
renderer (or (-> profile :props :renderer) :svg)
|
||||
cjk-feature? (features/use-feature "cjk-support")]
|
||||
(mf/use-effect
|
||||
#(dom/set-html-title (tr "title.settings.options")))
|
||||
|
||||
@ -176,7 +176,7 @@
|
||||
:id "options-section-title"}
|
||||
(tr "labels.settings")]
|
||||
[:> options-form*]]
|
||||
(when layout-feature?
|
||||
[:> language-layout-settings* {:config layout-config}])
|
||||
(when ^boolean cjk-feature?
|
||||
[:> text-language-settings* {:language (-> profile :props :default-text-language)}])
|
||||
(when (contains? cf/flags :render-switch)
|
||||
[:> webgl-settings* {:renderer renderer}])]]))
|
||||
|
||||
@ -31,9 +31,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings sections with standalone controls. */
|
||||
/* Copied from profile.scss .form-container, but without included nested
|
||||
rules, since we want custom styles for it */
|
||||
.webgl-container,
|
||||
.text-layout-container {
|
||||
.text-language-container {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
gap: var(--sp-s);
|
||||
@ -44,10 +45,8 @@
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.text-layout-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-s);
|
||||
.text-language-form {
|
||||
inline-size: $sz-252;
|
||||
padding-block-start: var(--sp-xs);
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.text.layout :as tl]
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
@ -231,28 +232,11 @@
|
||||
[:span {:class (stl/css :feedback :item-name)}
|
||||
(tr "labels.give-feedback")]])]))
|
||||
|
||||
(mf/defc language-layout-menu-item*
|
||||
[{:keys [language config can-edit read-only label on-toggle]}]
|
||||
(let [toggle (mf/use-fn
|
||||
(mf/deps language on-toggle)
|
||||
#(on-toggle language %))]
|
||||
(when (tl/file-action-visible? config can-edit read-only)
|
||||
[:> dropdown-menu-item* {:on-click toggle
|
||||
:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event) (toggle event)))
|
||||
:data-testid (str (name language) "-layout")
|
||||
:id (str "file-menu-" (name language) "-layout")}
|
||||
[:span {:class (stl/css :item-name)} label]])))
|
||||
|
||||
(mf/defc preferences-menu*
|
||||
{::mf/private true
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [layout-config layout profile toggle-flag
|
||||
on-close toggle-theme toggle-render toggle-text-layout]}]
|
||||
(let [renderer (or (-> profile :props :renderer) :svg)
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
can-edit (:can-edit (mf/deref refs/permissions))
|
||||
[{:keys [layout profile toggle-flag on-close toggle-theme toggle-render]}]
|
||||
(let [renderer (or (-> profile :props :renderer) :svg)
|
||||
|
||||
show-nudge-options
|
||||
(mf/use-fn
|
||||
@ -335,25 +319,6 @@
|
||||
:id "file-menu-nudge"}
|
||||
[:span {:class (stl/css :item-name)} (tr "modals.nudge-title")]]
|
||||
|
||||
[:> language-layout-menu-item*
|
||||
{:language :japanese :config (:japanese layout-config)
|
||||
:can-edit can-edit :read-only read-only? :on-toggle toggle-text-layout
|
||||
:label (if (get-in layout-config [:japanese :file?])
|
||||
(tr "workspace.header.menu.disable-japanese-layout")
|
||||
(tr "workspace.header.menu.enable-japanese-layout"))}]
|
||||
[:> language-layout-menu-item*
|
||||
{:language :chinese :config (:chinese layout-config)
|
||||
:can-edit can-edit :read-only read-only? :on-toggle toggle-text-layout
|
||||
:label (if (get-in layout-config [:chinese :file?])
|
||||
(tr "workspace.header.menu.disable-chinese-layout")
|
||||
(tr "workspace.header.menu.enable-chinese-layout"))}]
|
||||
[:> language-layout-menu-item*
|
||||
{:language :korean :config (:korean layout-config)
|
||||
:can-edit can-edit :read-only read-only? :on-toggle toggle-text-layout
|
||||
:label (if (get-in layout-config [:korean :file?])
|
||||
(tr "workspace.header.menu.disable-korean-layout")
|
||||
(tr "workspace.header.menu.enable-korean-layout"))}]
|
||||
|
||||
[:> dropdown-menu-item* {:on-click toggle-theme
|
||||
:class (stl/css :base-menu-item :submenu-item)
|
||||
:on-key-down (fn [event]
|
||||
@ -602,9 +567,78 @@
|
||||
(tr "workspace.header.menu.redo")]
|
||||
[:> shortcuts* {:id :redo}]])]))
|
||||
|
||||
(mf/defc file-language-menu*
|
||||
"File > Language: the language of the file texts without one of their own,
|
||||
and of the new texts. The choices open beside the item."
|
||||
{::mf/private true}
|
||||
[{:keys [on-close-all]}]
|
||||
(let [language (mf/deref refs/file-text-language)
|
||||
open* (mf/use-state false)
|
||||
open? (deref open*)
|
||||
options (mf/with-memo [language] (tl/language-options language))
|
||||
|
||||
show (mf/use-fn #(reset! open* true))
|
||||
hide (mf/use-fn #(reset! open* false))
|
||||
|
||||
toggle
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! open* not)))
|
||||
|
||||
on-select
|
||||
(mf/use-fn
|
||||
(mf/deps on-close-all)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(let [tag (dom/get-data (dom/get-current-target event) "language")]
|
||||
(st/emit! (-> (dw/set-default-text-language (when (not= "none" tag) tag))
|
||||
(vary-meta assoc ::ev/origin "workspace:menu")))
|
||||
(on-close-all))))]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item :language-menu-item)
|
||||
:on-click toggle
|
||||
:on-pointer-enter show
|
||||
:on-pointer-leave hide
|
||||
:aria-haspopup "menu"
|
||||
:aria-expanded open?
|
||||
:id "file-menu-language"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.header.menu.option.language")]
|
||||
[:> icon* {:icon-id i/arrow-right
|
||||
:class (stl/css :item-arrow)}]
|
||||
|
||||
(when ^boolean open?
|
||||
[:div {:class (stl/css :language-submenu)}
|
||||
[:ul {:class (stl/css :base-menu :language-submenu-list)
|
||||
:role "menu"}
|
||||
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :submenu-item)
|
||||
:data-language "none"
|
||||
:on-click on-select
|
||||
:id "file-menu-language-none"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "workspace.header.menu.language-none")]
|
||||
(when (nil? language)
|
||||
[:> icon* {:icon-id i/tick :class (stl/css :item-icon)}])]
|
||||
|
||||
(for [{:keys [id label header selected]} options]
|
||||
(if ^boolean header
|
||||
[:li {:key id
|
||||
:class (stl/css :language-header)
|
||||
:role "presentation"}
|
||||
label]
|
||||
[:> dropdown-menu-item* {:key id
|
||||
:class (stl/css :base-menu-item :submenu-item)
|
||||
:data-language id
|
||||
:on-click on-select
|
||||
:id (str "file-menu-language-" id)}
|
||||
[:span {:class (stl/css :item-name)} label]
|
||||
(when ^boolean selected
|
||||
[:> icon* {:icon-id i/tick :class (stl/css :item-icon)}])]))]])]))
|
||||
|
||||
(mf/defc file-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [on-close file]}]
|
||||
[{:keys [on-close on-close-all file]}]
|
||||
(let [file-id (:id file)
|
||||
shared? (:is-shared file)
|
||||
|
||||
@ -619,6 +653,7 @@
|
||||
|
||||
perms (mf/use-ctx ctx/permissions)
|
||||
can-edit (:can-edit perms)
|
||||
cjk-feature? (features/use-feature "cjk-support")
|
||||
|
||||
on-remove-shared
|
||||
(mf/use-fn
|
||||
@ -780,7 +815,12 @@
|
||||
:on-key-down on-export-frames-key-down
|
||||
:id "file-menu-export-frames"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.export-frames")]])]))
|
||||
(tr "dashboard.export-frames")]])
|
||||
|
||||
(when (and can-edit ^boolean cjk-feature? ^boolean (wm/vertical-layout-active?))
|
||||
[:*
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:> file-language-menu* {:on-close-all on-close-all}]])]))
|
||||
|
||||
(mf/defc plugins-menu*
|
||||
{::mf/private true
|
||||
@ -923,7 +963,6 @@
|
||||
(mf/defc menu*
|
||||
[{:keys [layout file]}]
|
||||
(let [profile (mf/deref refs/profile)
|
||||
layout-config (mf/deref refs/text-layout-config)
|
||||
|
||||
show-menu* (mf/use-state false)
|
||||
show-menu? (deref show-menu*)
|
||||
@ -985,15 +1024,6 @@
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (du/toggle-theme))))
|
||||
|
||||
toggle-text-layout
|
||||
(mf/use-fn
|
||||
(mf/deps layout-config)
|
||||
(fn [language event]
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dw/set-text-layout language (not (get-in layout-config [language :file?]))))
|
||||
(reset! show-menu* false)
|
||||
(reset! selected-sub-menu* nil)))
|
||||
|
||||
toggle-render
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
@ -1179,7 +1209,8 @@
|
||||
(case selected-sub-menu
|
||||
:file
|
||||
[:> file-menu* {:file file
|
||||
:on-close close-sub-menu}]
|
||||
:on-close close-sub-menu
|
||||
:on-close-all close-all-menus}]
|
||||
|
||||
:edit
|
||||
[:> edit-menu* {:on-close close-sub-menu}]
|
||||
@ -1193,12 +1224,10 @@
|
||||
:preferences
|
||||
[:> preferences-menu* {:layout layout
|
||||
:profile profile
|
||||
:layout-config layout-config
|
||||
:toggle-flag toggle-flag
|
||||
:toggle-theme toggle-theme
|
||||
:toggle-render toggle-render
|
||||
:show-shortcuts show-shortcuts
|
||||
:toggle-text-layout toggle-text-layout
|
||||
:on-close close-sub-menu}]
|
||||
|
||||
:plugins
|
||||
|
||||
@ -194,3 +194,39 @@
|
||||
background-color: var(--color-background-info);
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
// File > Language: its choices open beside the item. The padding bridges the
|
||||
// gap to the item, so moving the pointer there keeps the list open.
|
||||
.language-menu-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-submenu {
|
||||
position: absolute;
|
||||
inset-block-start: calc(-1 * (var(--sp-xs) + $b-2));
|
||||
inset-inline-start: 100%;
|
||||
padding-inline-start: var(--sp-s);
|
||||
}
|
||||
|
||||
// The list sits inside the Language item, so it would inherit the colors
|
||||
// that item sets on hover; it takes back the design-token defaults and its
|
||||
// own items get their hover again.
|
||||
.language-submenu-list {
|
||||
--menu-background-color: var(--color-background-tertiary);
|
||||
--menu-foreground-color: var(--color-foreground-primary);
|
||||
--menu-icon-foreground-color: var(--color-foreground-secondary);
|
||||
--menu-shortcut-foreground-color: var(--color-foreground-secondary);
|
||||
|
||||
position: relative;
|
||||
min-inline-size: $sz-192;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.language-header {
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
padding: var(--sp-xs) $sz-6;
|
||||
color: var(--color-foreground-secondary);
|
||||
text-transform: uppercase;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.text.cjk-layout :as cjk]
|
||||
[app.config :as cf]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace :as dw]
|
||||
@ -24,6 +23,7 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@ -351,7 +351,7 @@
|
||||
|
||||
clip-id (dm/str "text-edition-clip" shape-id)
|
||||
|
||||
vertical? (cjk/vertical-text-content? (:content shape))
|
||||
vertical? (wm/vertical-text-content? (:content shape))
|
||||
|
||||
contenteditable-ref (mf/use-ref nil)
|
||||
|
||||
|
||||
@ -23,10 +23,9 @@
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.grouped-dropdown :refer [grouped-dropdown*]]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.components.search-bar :refer [search-bar*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
@ -136,6 +135,20 @@
|
||||
acc))))
|
||||
acc)))))
|
||||
|
||||
(defn size-preset-options
|
||||
"Rows of the size presets dropdown: a preset without `:width` heads the
|
||||
presets that follow it. A preset is selected when it has the shape size."
|
||||
[presets width height]
|
||||
(mapv (fn [preset]
|
||||
(let [preset-name (:name preset)]
|
||||
(if (:width preset)
|
||||
{:id preset-name
|
||||
:label preset-name
|
||||
:detail (str (:width preset) " x " (:height preset))
|
||||
:selected (and (= width (:width preset)) (= height (:height preset)))}
|
||||
{:id preset-name :label preset-name :header true})))
|
||||
presets))
|
||||
|
||||
(defn check-measures-menu-props
|
||||
[old-props new-props]
|
||||
(let [o-values (unchecked-get old-props "values")
|
||||
@ -366,16 +379,18 @@
|
||||
(mf/with-memo [preset-search-term]
|
||||
(filter-size-presets preset-search-term size-presets))
|
||||
|
||||
preset-width (d/parse-integer (:width values) 0)
|
||||
preset-height (d/parse-integer (:height values) 0)
|
||||
|
||||
preset-options
|
||||
(mf/with-memo [filtered-size-presets preset-width preset-height]
|
||||
(size-preset-options filtered-size-presets preset-width preset-height))
|
||||
|
||||
on-preset-selected
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [event]
|
||||
(let [width (-> (dom/get-current-target event)
|
||||
(dom/get-data "width")
|
||||
(d/read-string))
|
||||
height (-> (dom/get-current-target event)
|
||||
(dom/get-data "height")
|
||||
(d/read-string))]
|
||||
(fn [preset-name _event]
|
||||
(let [{:keys [width height]} (d/seek #(= preset-name (:name %)) size-presets)]
|
||||
(st/emit! (udw/update-dimensions ids :width width)
|
||||
(udw/update-dimensions ids :height height))
|
||||
(reset! preset-state* false)
|
||||
@ -498,42 +513,15 @@
|
||||
[:span {:class (stl/css :select-name)} (tr "workspace.options.size-presets")]
|
||||
[:span {:class (stl/css :collapsed-icon)} deprecated-icon/arrow]
|
||||
|
||||
[:& dropdown {:show show-presets-dropdown?
|
||||
:on-close close-presets
|
||||
:container preset-container-ref}
|
||||
[:div {:class (stl/css :custom-select-dropdown)
|
||||
:on-click dom/stop-propagation}
|
||||
[:div {:class (stl/css :preset-search)}
|
||||
[:> search-bar* {:on-change on-preset-search-change
|
||||
:value preset-search-term
|
||||
:auto-focus true
|
||||
:placeholder (tr "workspace.options.search-size-preset")}]]
|
||||
[:ul {:class (stl/css :preset-list)}
|
||||
(if (empty? filtered-size-presets)
|
||||
[:li {:class (stl/css-case :dropdown-element true
|
||||
:disabled true)}
|
||||
[:span {:class (stl/css :preset-name)}
|
||||
(tr "workspace.options.no-size-preset-results")]]
|
||||
(for [size-preset filtered-size-presets]
|
||||
(if-not (:width size-preset)
|
||||
[:li {:key (:name size-preset)
|
||||
:class (stl/css-case :dropdown-element true
|
||||
:disabled true)}
|
||||
[:span {:class (stl/css :preset-name)} (:name size-preset)]]
|
||||
|
||||
(let [preset-match (and (= (:width size-preset) (d/parse-integer (:width values) 0))
|
||||
(= (:height size-preset) (d/parse-integer (:height values) 0)))]
|
||||
[:li {:key (:name size-preset)
|
||||
:class (stl/css-case :dropdown-element true
|
||||
:match preset-match)
|
||||
:data-width (str (:width size-preset))
|
||||
:data-height (str (:height size-preset))
|
||||
:on-click on-preset-selected}
|
||||
[:div {:class (stl/css :name-wrapper)}
|
||||
[:span {:class (stl/css :preset-name)} (:name size-preset)]
|
||||
[:span {:class (stl/css :preset-size)} (:width size-preset) " x " (:height size-preset)]]
|
||||
(when preset-match
|
||||
[:span {:class (stl/css :check-icon)} deprecated-icon/tick])]))))]]]]
|
||||
[:> grouped-dropdown* {:show show-presets-dropdown?
|
||||
:on-close close-presets
|
||||
:container preset-container-ref
|
||||
:options preset-options
|
||||
:on-select on-preset-selected
|
||||
:search-term preset-search-term
|
||||
:search-placeholder (tr "workspace.options.search-size-preset")
|
||||
:on-search-change on-preset-search-change
|
||||
:empty-label (tr "workspace.options.no-size-preset-results")}]]
|
||||
|
||||
[:& radio-buttons {:selected (or (d/name orientation) "")
|
||||
:on-change on-orientation-change
|
||||
|
||||
@ -69,90 +69,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-select-dropdown {
|
||||
@extend %dropdown-wrapper;
|
||||
|
||||
margin-top: deprecated.$s-2;
|
||||
max-height: 70vh;
|
||||
width: deprecated.$s-252;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preset-search {
|
||||
padding: deprecated.$s-4;
|
||||
border-bottom: deprecated.$s-1 solid var(--menu-border-color-rest, transparent);
|
||||
}
|
||||
|
||||
.preset-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
.dropdown-element {
|
||||
@extend %dropdown-element-base;
|
||||
|
||||
.name-wrapper {
|
||||
display: flex;
|
||||
gap: deprecated.$s-8;
|
||||
flex-grow: 1;
|
||||
|
||||
.preset-name {
|
||||
color: var(--menu-foreground-color-rest);
|
||||
}
|
||||
|
||||
.preset-size {
|
||||
color: var(--menu-foreground-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
@include deprecated.flex-center;
|
||||
|
||||
svg {
|
||||
@extend %button-icon-small;
|
||||
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
|
||||
.preset-name {
|
||||
color: var(--menu-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.match {
|
||||
.name-wrapper .preset-name {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-background-color-hover);
|
||||
|
||||
.name-wrapper .preset-name {
|
||||
color: var(--menu-foreground-color-hover);
|
||||
}
|
||||
|
||||
.check-icon svg {
|
||||
stroke: var(--menu-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.size,
|
||||
.position,
|
||||
.rotation-radius {
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.sidebar.options.common :refer [radio-selected]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.text-japanese-layout :as tjl]
|
||||
[app.main.ui.workspace.sidebar.options.menus.text-language-layout :as tll]
|
||||
[app.main.ui.workspace.sidebar.options.menus.token-typography-row :refer [token-typography-row*]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.typography :refer [text-options* typography-entry*]]
|
||||
@ -40,7 +39,6 @@
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.text.content :as content]
|
||||
[app.util.text.layout :as tl]
|
||||
[app.util.text.ui :as txu]
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[app.util.timers :as ts]
|
||||
@ -59,8 +57,7 @@
|
||||
|
||||
(mf/defc text-align-options*
|
||||
[{:keys [values on-change on-blur]}]
|
||||
(let [vertical? (and (wm/vertical-layout-active?)
|
||||
(tjl/vertical-japanese-layout? values))
|
||||
(let [vertical? (wm/vertical-values? values)
|
||||
options
|
||||
(mf/with-memo [vertical?]
|
||||
[{:value "left"
|
||||
@ -101,8 +98,7 @@
|
||||
|
||||
(mf/defc text-direction-options*
|
||||
[{:keys [values on-change on-blur]}]
|
||||
(let [vertical? (and (wm/vertical-layout-active?)
|
||||
(tjl/vertical-japanese-layout? values))
|
||||
(let [vertical? (wm/vertical-values? values)
|
||||
direction (radio-selected (:text-direction values))
|
||||
options
|
||||
(mf/with-memo []
|
||||
@ -131,8 +127,7 @@
|
||||
|
||||
(mf/defc vertical-align*
|
||||
[{:keys [values on-change on-blur]}]
|
||||
(let [vertical? (and (wm/vertical-layout-active?)
|
||||
(tjl/vertical-japanese-layout? values))
|
||||
(let [vertical? (wm/vertical-values? values)
|
||||
vertical-align (radio-selected (:vertical-align values) "top")
|
||||
options
|
||||
(mf/with-memo [vertical?]
|
||||
@ -170,8 +165,7 @@
|
||||
|
||||
(mf/defc grow-options*
|
||||
[{:keys [ids values on-blur]}]
|
||||
(let [vertical? (and (wm/vertical-layout-active?)
|
||||
(tjl/vertical-japanese-layout? values))
|
||||
(let [vertical? (wm/vertical-values? values)
|
||||
grow-type (:grow-type values)
|
||||
selected (radio-selected grow-type)
|
||||
editor-instance (mf/deref refs/workspace-editor)
|
||||
@ -371,26 +365,6 @@
|
||||
;; Main component
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(mf/defc language-layout-section*
|
||||
[{:keys [language title config options-props]}]
|
||||
(let [open* (mf/use-state true)
|
||||
open? (deref open*)
|
||||
toggle-open (mf/use-fn #(swap! open* not))]
|
||||
(when (tl/controls-enabled? config)
|
||||
[:section {:class (stl/css :element-set)
|
||||
:data-keep-editing-on-blur true
|
||||
:aria-label title}
|
||||
[:div {:class (stl/css :element-title)}
|
||||
[:> title-bar* {:collapsable true
|
||||
:collapsed (not open?)
|
||||
:on-collapsed toggle-open
|
||||
:title title
|
||||
:class (stl/css :title-spacing-text)}]]
|
||||
(when open?
|
||||
[:div {:class (stl/css :element-content)}
|
||||
[:> tll/language-layout-options*
|
||||
(mf/spread-props options-props {:language language})]])])))
|
||||
|
||||
(mf/defc text-menu*
|
||||
{::mf/wrap [#(mf/memo' % check-props)]}
|
||||
[{:keys [ids type values ruby-values text-selection-active reading-selection-active zhuyin-selection-active
|
||||
@ -402,7 +376,7 @@
|
||||
menu-state (deref menu-state*)
|
||||
main-menu-open? (:main-menu menu-state)
|
||||
more-options-open? (:more-options menu-state)
|
||||
layout-config (mf/deref refs/text-layout-config)
|
||||
cjk-feature? (features/use-feature "cjk-support")
|
||||
|
||||
font-id (or (:font-id values) (:font-id txt/default-typography))
|
||||
|
||||
@ -713,15 +687,7 @@
|
||||
[:> text-decoration-options* (mf/spread-props common-props {:token-applied current-token-name})]
|
||||
[:> text-direction-options* common-props]])])]
|
||||
|
||||
[:> language-layout-section* {:language :japanese
|
||||
:title (tr "workspace.options.text-options.japanese-layout")
|
||||
:config (:japanese layout-config)
|
||||
:options-props language-layout-props}]
|
||||
[:> language-layout-section* {:language :chinese
|
||||
:title (tr "workspace.options.text-options.chinese-layout")
|
||||
:config (:chinese layout-config)
|
||||
:options-props language-layout-props}]
|
||||
[:> language-layout-section* {:language :korean
|
||||
:title (tr "workspace.options.text-options.korean-layout")
|
||||
:config (:korean layout-config)
|
||||
:options-props language-layout-props}]]))
|
||||
(when (and ^boolean cjk-feature? ^boolean (wm/vertical-layout-active?))
|
||||
[:> tll/language-section* {:ids ids
|
||||
:options-props language-layout-props
|
||||
:on-change on-change}])]))
|
||||
|
||||
@ -181,10 +181,91 @@
|
||||
[{:id "auto" :label (translate "workspace.options.text-options.ruby-overhang-auto")}
|
||||
{:id "none" :label (translate "workspace.options.text-options.ruby-overhang-none")}])
|
||||
|
||||
(defn ruby-side-options
|
||||
[translate]
|
||||
[{:id "over" :label (translate "workspace.options.text-options.ruby-side-over")}
|
||||
{:id "under" :label (translate "workspace.options.text-options.ruby-side-under")}])
|
||||
;; --- 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
|
||||
;; in horizontal text, right/left in vertical text. The controls show the
|
||||
;; one that applies and keep the stored other.
|
||||
|
||||
(defn- position-sides
|
||||
"Block (`\"over\"`/`\"under\"`) and inline (`\"right\"`/`\"left\"`) side
|
||||
of a position `value`, nil for the ones it leaves to the renderer."
|
||||
[attr value]
|
||||
(case attr
|
||||
:ruby-side
|
||||
[value nil]
|
||||
|
||||
:text-emphasis-position
|
||||
(when (and (string? value) (not= "auto" value))
|
||||
(let [[block inline] (.split value "-")] [block inline]))
|
||||
|
||||
:text-underline-position
|
||||
(case value
|
||||
"under" ["under" nil]
|
||||
"left" [nil "left"]
|
||||
"right" [nil "right"]
|
||||
"under-left" ["under" "left"]
|
||||
"under-right" ["under" "right"]
|
||||
nil)))
|
||||
|
||||
(defn- position-of-sides
|
||||
[attr [block inline]]
|
||||
(case attr
|
||||
:ruby-side block
|
||||
:text-emphasis-position (if (some? block) (str block "-" inline) "auto")
|
||||
:text-underline-position (cond
|
||||
(and block inline) (str block "-" inline)
|
||||
(or block inline) (or block inline)
|
||||
:else "auto")))
|
||||
|
||||
(defn position-options
|
||||
"Choices of the position `attr` for the writing mode."
|
||||
[attr vertical]
|
||||
(let [auto {:id "auto" :label (tr "workspace.options.text-options.position-auto")}
|
||||
over {:id "over" :label (tr "workspace.options.text-options.position-over")}
|
||||
under {:id "under" :label (tr "workspace.options.text-options.position-under")}
|
||||
right {:id "right" :label (tr "workspace.options.text-options.position-right")}
|
||||
left {:id "left" :label (tr "workspace.options.text-options.position-left")}
|
||||
;; Ruby takes the block side in vertical text too: over reads right.
|
||||
ruby-v [(assoc right :id "over") (assoc left :id "under")]]
|
||||
(case attr
|
||||
:ruby-side (if ^boolean vertical ruby-v [over under])
|
||||
:text-emphasis-position (if ^boolean vertical [auto right left] [auto over under])
|
||||
:text-underline-position (if ^boolean vertical [auto right left] [auto under]))))
|
||||
|
||||
(defn position-choice
|
||||
"Choice of `position-options` a stored position `value` shows."
|
||||
[attr vertical value]
|
||||
(if (mixed-span-value? value)
|
||||
value
|
||||
(let [[block inline] (position-sides attr value)]
|
||||
(cond
|
||||
(= :ruby-side attr) (or block (cjk/enum-default :ruby-side))
|
||||
^boolean vertical (or inline "auto")
|
||||
:else (or block "auto")))))
|
||||
|
||||
(defn position-value
|
||||
"Stored position for picking `choice` over the stored `value`: the side of
|
||||
the other writing mode stays as it was."
|
||||
[attr vertical value choice]
|
||||
(cond
|
||||
(= :ruby-side attr)
|
||||
choice
|
||||
|
||||
;; Emphasis is auto as a whole, or sets both sides.
|
||||
(= :text-emphasis-position attr)
|
||||
(if (= "auto" choice)
|
||||
"auto"
|
||||
(let [[block inline] (when-not (mixed-span-value? value) (position-sides attr value))]
|
||||
(position-of-sides attr (if ^boolean vertical
|
||||
[(or block "over") choice]
|
||||
[choice (or inline "right")]))))
|
||||
|
||||
:else
|
||||
(let [[block inline] (when-not (mixed-span-value? value) (position-sides attr value))
|
||||
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;
|
||||
@ -307,22 +388,43 @@
|
||||
: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}]]))
|
||||
|
||||
(defn- ruby-common-props
|
||||
[values on-change on-blur labels]
|
||||
[values on-change on-blur labels vertical]
|
||||
(mf/props
|
||||
{:values values
|
||||
:on-change on-change
|
||||
:on-blur on-blur
|
||||
:labels labels}))
|
||||
:labels labels
|
||||
:vertical vertical}))
|
||||
|
||||
(mf/defc ruby-hidden-option*
|
||||
[{:keys [values on-change on-blur labels]}]
|
||||
[{:keys [values on-change on-blur]}]
|
||||
(let [value (:ruby-hidden values)
|
||||
enabled? (cond
|
||||
(mixed-span-value? value) nil
|
||||
(true? value) true
|
||||
:else false)
|
||||
label (or (:hidden labels) (tr "workspace.options.text-options.ruby-hidden"))
|
||||
label (tr "workspace.options.text-options.ruby-hidden")
|
||||
handle-change
|
||||
(mf/use-fn
|
||||
(mf/deps on-change on-blur)
|
||||
@ -336,42 +438,39 @@
|
||||
:on-change handle-change}]]))
|
||||
|
||||
(mf/defc ruby-customization-options*
|
||||
[{:keys [values on-change on-blur labels]}]
|
||||
(let [common-props (ruby-common-props values on-change on-blur labels)
|
||||
[{:keys [values on-change on-blur labels vertical]}]
|
||||
(let [common-props (ruby-common-props values on-change on-blur labels vertical)
|
||||
options (mf/with-memo []
|
||||
{:ruby-size (ruby-size-options tr)
|
||||
:ruby-align (ruby-align-options tr)
|
||||
:ruby-overhang (ruby-overhang-options tr)
|
||||
:ruby-side (ruby-side-options tr)})]
|
||||
:ruby-overhang (ruby-overhang-options tr)})]
|
||||
[:div {:class (stl/css :japanese-layout-controls)}
|
||||
[:> ruby-hidden-option* common-props]
|
||||
[:> span-select-option* (mf/spread-props common-props
|
||||
{:attr :ruby-size
|
||||
:default-value (cjk/enum-default :ruby-size)
|
||||
:label (or (:size labels) (tr "workspace.options.text-options.ruby-size"))
|
||||
:label (tr "workspace.options.text-options.ruby-size")
|
||||
:options (:ruby-size options)})]
|
||||
[:> span-select-option* (mf/spread-props common-props
|
||||
{:attr :ruby-align
|
||||
:default-value (cjk/enum-default :ruby-align)
|
||||
:label (or (:align labels) (tr "workspace.options.text-options.ruby-align"))
|
||||
:label (tr "workspace.options.text-options.ruby-align")
|
||||
:options (:ruby-align options)})]
|
||||
[:> span-select-option* (mf/spread-props common-props
|
||||
{:attr :ruby-overhang
|
||||
:default-value (cjk/enum-default :ruby-overhang)
|
||||
:label (or (:overhang labels) (tr "workspace.options.text-options.ruby-overhang"))
|
||||
:label (tr "workspace.options.text-options.ruby-overhang")
|
||||
:options (:ruby-overhang options)})]
|
||||
[:> span-select-option* (mf/spread-props common-props
|
||||
{:attr :ruby-side
|
||||
:default-value (cjk/enum-default :ruby-side)
|
||||
:label (or (:side labels) (tr "workspace.options.text-options.ruby-side"))
|
||||
:options (:ruby-side options)})]]))
|
||||
[:> position-select-option* (mf/spread-props common-props
|
||||
{:attr :ruby-side
|
||||
:label (tr "workspace.options.text-options.ruby-side")})]]))
|
||||
|
||||
(mf/defc ruby-advanced-options*
|
||||
[{:keys [values on-change on-blur labels]}]
|
||||
[{:keys [values on-change on-blur labels vertical]}]
|
||||
(let [open* (mf/use-state false)
|
||||
open? (deref open*)
|
||||
toggle-open (mf/use-fn #(swap! open* not))
|
||||
common-props (ruby-common-props values on-change on-blur labels)]
|
||||
common-props (ruby-common-props values on-change on-blur labels vertical)]
|
||||
[:div {:class (stl/css :ruby-advanced-options)}
|
||||
[:> ruby-options* (mf/spread-props common-props
|
||||
{:advanced-open open?
|
||||
@ -381,11 +480,11 @@
|
||||
[:> ruby-customization-options* common-props]]]))
|
||||
|
||||
(mf/defc ruby-presentation-options*
|
||||
[{:keys [values on-change on-blur labels]}]
|
||||
[{:keys [values on-change on-blur labels vertical]}]
|
||||
(let [open* (mf/use-state false)
|
||||
open? (deref open*)
|
||||
toggle-open (mf/use-fn #(swap! open* not))
|
||||
common-props (ruby-common-props values on-change on-blur labels)]
|
||||
common-props (ruby-common-props values on-change on-blur labels vertical)]
|
||||
[:div {:class (stl/css :ruby-advanced-options)}
|
||||
[:> title-bar* {:collapsable true
|
||||
:collapsed (not open?)
|
||||
@ -475,17 +574,20 @@
|
||||
: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]}]
|
||||
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})
|
||||
:on-blur on-blur
|
||||
:vertical vertical?})
|
||||
ruby-presentation-props (mf/props
|
||||
{:values ruby-values
|
||||
:on-change on-ruby-presentation-change
|
||||
:on-blur on-blur})
|
||||
:on-blur on-blur
|
||||
:vertical vertical?})
|
||||
options (mf/with-memo []
|
||||
{:writing-mode (writing-mode-options tr)
|
||||
:text-orientation (text-orientation-options tr)
|
||||
@ -522,6 +624,7 @@
|
||||
: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]
|
||||
|
||||
@ -8,44 +8,21 @@
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.types.text.cjk-layout :as cjk]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.grouped-dropdown :refer [grouped-dropdown*]]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[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.util.i18n :refer [tr]]
|
||||
[app.util.text.layout :as tl]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Id of the "No language profile" choice. The DS select shows "--" for a
|
||||
;; blank id, so the choice cannot use "".
|
||||
(def plain-profile "plain")
|
||||
|
||||
(defn selected-profile
|
||||
"Select id of a paragraph `profile`: the plain choice when unset."
|
||||
[profile]
|
||||
(if (= :multiple profile) "multiple" (or profile plain-profile)))
|
||||
|
||||
(defn profile-options
|
||||
[language value translate]
|
||||
(let [options (into [{:id plain-profile :label (translate "workspace.options.text-options.profile-plain")}]
|
||||
(case language
|
||||
:japanese [{:id "ja" :label (translate "workspace.options.text-options.profile-japanese")}]
|
||||
:chinese [{:id "zh-Hans" :label (translate "workspace.options.text-options.profile-chinese-simplified")}
|
||||
{:id "zh-Hant" :label (translate "workspace.options.text-options.profile-chinese-traditional")}
|
||||
{:id "zh-Hans-CN" :label (translate "workspace.options.text-options.profile-chinese-mainland-simplified")}
|
||||
{:id "zh-Hant-CN" :label (translate "workspace.options.text-options.profile-chinese-mainland-traditional")}
|
||||
{:id "zh-Hant-TW" :label (translate "workspace.options.text-options.profile-chinese-taiwan")}
|
||||
{:id "zh-Hant-HK" :label (translate "workspace.options.text-options.profile-chinese-hong-kong")}]
|
||||
:korean [{:id "ko" :label (translate "workspace.options.text-options.profile-korean")}]))
|
||||
mixed? (= :multiple value)
|
||||
id (selected-profile value)]
|
||||
(cond-> options
|
||||
(not (some #(= (:id %) id) options))
|
||||
(conj {:id id
|
||||
:label (if mixed? (translate "settings.multiple") value)
|
||||
:disabled true
|
||||
:dimmed true}))))
|
||||
|
||||
(defn korean-wrap-options
|
||||
[value translate]
|
||||
(cond-> [{:id "syllable" :label (translate "workspace.options.text-options.korean-wrap-syllable")}
|
||||
@ -53,43 +30,21 @@
|
||||
(= value "spaces-only")
|
||||
(conj {:id "spaces-only" :label (translate "workspace.options.text-options.korean-wrap-spaces-only")})))
|
||||
|
||||
(defn profile-change
|
||||
[value]
|
||||
{:text-language (when (and (seq value) (not= plain-profile value)) value)})
|
||||
|
||||
(defn emphasis-position-options
|
||||
[translate]
|
||||
[{:id "auto" :label (translate "workspace.options.text-options.position-auto")}
|
||||
{:id "over-right" :label (translate "workspace.options.text-options.position-over-right")}
|
||||
{:id "over-left" :label (translate "workspace.options.text-options.position-over-left")}
|
||||
{:id "under-right" :label (translate "workspace.options.text-options.position-under-right")}
|
||||
{:id "under-left" :label (translate "workspace.options.text-options.position-under-left")}])
|
||||
|
||||
(defn underline-position-options
|
||||
[translate]
|
||||
[{:id "auto" :label (translate "workspace.options.text-options.position-auto")}
|
||||
{:id "under" :label (translate "workspace.options.text-options.position-under")}
|
||||
{:id "left" :label (translate "workspace.options.text-options.position-left")}
|
||||
{:id "right" :label (translate "workspace.options.text-options.position-right")}
|
||||
{:id "under-left" :label (translate "workspace.options.text-options.position-under-left")}
|
||||
{:id "under-right" :label (translate "workspace.options.text-options.position-under-right")}])
|
||||
|
||||
(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})]
|
||||
(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/span-select-option*
|
||||
(mf/spread-props props {:attr :text-emphasis-position :default-value "auto"
|
||||
:label (tr "workspace.options.text-options.emphasis-position")
|
||||
:options (emphasis-position-options tr)})]
|
||||
[:> tjl/span-select-option*
|
||||
(mf/spread-props props {:attr :text-underline-position :default-value "auto"
|
||||
:label (tr "workspace.options.text-options.underline-position")
|
||||
:options (underline-position-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")
|
||||
@ -101,22 +56,18 @@
|
||||
(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")
|
||||
:hidden (if zhuyin? (tr "workspace.options.text-options.zhuyin-hidden") (tr "workspace.options.text-options.pinyin-hidden"))
|
||||
:size (tr "workspace.options.text-options.pinyin-size")
|
||||
:align (tr "workspace.options.text-options.pinyin-align")
|
||||
:overhang (tr "workspace.options.text-options.pinyin-overhang")
|
||||
:side (tr "workspace.options.text-options.pinyin-side")}
|
||||
: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 :labels (cond-> labels zhuyin?
|
||||
(assoc :validate cjk/zhuyin-reading?
|
||||
:invalid (tr "workspace.options.text-options.zhuyin-invalid")))})]
|
||||
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 :profile-option)}
|
||||
[: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")
|
||||
@ -160,45 +111,149 @@
|
||||
: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 [profile (:text-language values)
|
||||
selected (selected-profile profile)
|
||||
options (mf/with-memo [language profile] (profile-options language profile tr))
|
||||
matches? (tl/profile-matches? language profile)
|
||||
handle-profile-change (mf/use-fn
|
||||
(mf/deps on-change)
|
||||
#(on-change (profile-change %)))
|
||||
props (mf/props {:values values :on-change on-change :on-blur on-blur})]
|
||||
(let [props (mf/props {:values values :on-change on-change :on-blur on-blur})]
|
||||
[:div {:class (stl/css :language-layout-options)}
|
||||
[:div {:class (stl/css :profile-option)}
|
||||
[:span {:class (stl/css :option-label)}
|
||||
(tr "workspace.options.text-options.language-profile")]
|
||||
[:> select* {:default-selected selected
|
||||
:aria-label (tr "workspace.options.text-options.language-profile")
|
||||
:options options
|
||||
:on-change handle-profile-change}]]
|
||||
(when matches?
|
||||
[:> distribute-option* props])
|
||||
(if (and (= :japanese language) matches?)
|
||||
;; 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})]
|
||||
[:> tjl/writing-mode-controls* props])
|
||||
(when (and (= :chinese language) matches?)
|
||||
: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 (and (= :chinese language) matches?)
|
||||
(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 (and (= :korean language) matches?)
|
||||
(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)})])]))
|
||||
|
||||
(mf/defc language-row*
|
||||
"The language of the texts, opening the language dropdown, and the button
|
||||
removing it when the texts have a language of their own."
|
||||
{::mf/private true}
|
||||
[{:keys [language inherited open on-toggle on-remove children]}]
|
||||
[:div {:class (stl/css :language-row)}
|
||||
[:button {:type "button"
|
||||
:class (stl/css-case :language-select true
|
||||
:mixed (= :multiple language))
|
||||
:aria-haspopup "listbox"
|
||||
:aria-expanded open
|
||||
:aria-label (tr "workspace.options.text-options.language")
|
||||
:data-testid "text-language-select"
|
||||
:on-click on-toggle}
|
||||
[:span {:class (stl/css :language-name)} (tl/language-label language)]
|
||||
[:> icon* {:icon-id i/arrow-down
|
||||
:class (stl/css :language-arrow)
|
||||
:size "s"
|
||||
:aria-hidden true}]]
|
||||
(when-not ^boolean inherited
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "workspace.options.text-options.remove-language")
|
||||
:data-testid "text-language-remove"
|
||||
:on-click on-remove
|
||||
:icon i/remove}])
|
||||
children])
|
||||
|
||||
(mf/defc language-section*
|
||||
"Language panel of the text options. A text has one language: picking or
|
||||
removing one changes every paragraph of the texts in `ids`, and new texts
|
||||
take it. A text without language takes the file one. The options of the
|
||||
language show once it is the same for all of them."
|
||||
[{:keys [ids options-props on-change]}]
|
||||
(let [language-ref (mf/with-memo [ids] (refs/texts-language ids))
|
||||
{:keys [language own]} (mf/deref language-ref)
|
||||
family (cjk/language-family language)
|
||||
inherited (and (some? language) (nil? own))
|
||||
|
||||
open* (mf/use-state true)
|
||||
open? (deref open*)
|
||||
dropdown* (mf/use-state false)
|
||||
dropdown? (deref dropdown*)
|
||||
container-ref (mf/use-ref nil)
|
||||
|
||||
title (tr "workspace.options.text-options.language")
|
||||
options (mf/with-memo [language] (tl/language-options language))
|
||||
|
||||
toggle-open
|
||||
(mf/use-fn #(swap! open* not))
|
||||
|
||||
toggle-dropdown
|
||||
(mf/use-fn #(swap! dropdown* not))
|
||||
|
||||
close-dropdown
|
||||
(mf/use-fn #(reset! dropdown* false))
|
||||
|
||||
handle-select
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
(fn [tag _event]
|
||||
(reset! dropdown* false)
|
||||
(reset! open* true)
|
||||
(on-change {:text-language tag})
|
||||
(st/emit! (dwt/remember-text-language tag))))
|
||||
|
||||
handle-remove
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
(fn [_event]
|
||||
(reset! dropdown* false)
|
||||
(on-change {:text-language nil})
|
||||
(st/emit! (dwt/remember-text-language nil))))
|
||||
|
||||
;; Opens below the control that toggles it: the add button of the
|
||||
;; title, or the language row.
|
||||
dropdown
|
||||
(mf/html
|
||||
[:> grouped-dropdown* {:show dropdown?
|
||||
:on-close close-dropdown
|
||||
:container container-ref
|
||||
:options options
|
||||
:on-select handle-select}])]
|
||||
|
||||
[:section {:class (stl/css :language-section)
|
||||
:data-keep-editing-on-blur true
|
||||
:aria-label title
|
||||
:ref container-ref}
|
||||
[:div {:class (stl/css :language-title)}
|
||||
[:> title-bar* {:collapsable (some? language)
|
||||
:collapsed (not open?)
|
||||
:on-collapsed toggle-open
|
||||
:title title}
|
||||
(when (nil? language)
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "workspace.options.text-options.add-language")
|
||||
:aria-haspopup "listbox"
|
||||
:aria-expanded dropdown?
|
||||
:data-testid "text-language-add"
|
||||
:on-click toggle-dropdown
|
||||
:icon i/add}])]
|
||||
(when (nil? language)
|
||||
dropdown)]
|
||||
|
||||
(when (and (some? language) open?)
|
||||
[:div {:class (stl/css :language-content)}
|
||||
[:> language-row* {:language language
|
||||
:inherited inherited
|
||||
:open dropdown?
|
||||
:on-toggle toggle-dropdown
|
||||
:on-remove handle-remove}
|
||||
dropdown]
|
||||
(when (some? family)
|
||||
[:> language-layout-options*
|
||||
(mf/spread-props options-props {:language family})])])]))
|
||||
|
||||
@ -4,9 +4,85 @@
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
|
||||
.language-section {
|
||||
@include sidebar.option-grid-structure;
|
||||
}
|
||||
|
||||
.language-title {
|
||||
position: relative;
|
||||
grid-column: span 8;
|
||||
}
|
||||
|
||||
.language-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
grid-column: span 8;
|
||||
margin-block-start: var(--sp-xs);
|
||||
}
|
||||
|
||||
.language-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
// Looks like the DS select it stands for; it opens the grouped dropdown.
|
||||
.language-select {
|
||||
--language-select-background: var(--color-background-tertiary);
|
||||
--language-select-icon-color: var(--color-icon-default);
|
||||
--language-select-outline-color: none;
|
||||
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
block-size: $sz-32;
|
||||
padding: var(--sp-s);
|
||||
border: none;
|
||||
border-radius: $br-8;
|
||||
outline: $b-1 solid var(--language-select-outline-color);
|
||||
background: var(--language-select-background);
|
||||
color: var(--color-foreground-primary);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
|
||||
&:is(:hover, [aria-expanded="true"]) {
|
||||
--language-select-background: var(--color-background-quaternary);
|
||||
--language-select-icon-color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
--language-select-background: var(--color-background-quaternary);
|
||||
--language-select-outline-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.language-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-inline-start: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.mixed .language-name {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.language-arrow {
|
||||
color: var(--language-select-icon-color);
|
||||
}
|
||||
|
||||
.language-layout-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -14,7 +90,7 @@
|
||||
padding-block: var(--sp-xs);
|
||||
}
|
||||
|
||||
.profile-option {
|
||||
.reading-format-option {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
|
||||
align-items: center;
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
[app.util.modules :as mod]
|
||||
[app.util.text.content :as tc]
|
||||
[app.util.text.geometry :as text.geom]
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@ -339,7 +340,7 @@
|
||||
|
||||
;; forward declare helpers so render can call them
|
||||
(declare request-render)
|
||||
(declare set-shape-vertical-align set-shape-line-adjustment fonts-from-text-content)
|
||||
(declare set-shape-vertical-align set-shape-line-adjustment fonts-from-text-content ensure-text-content)
|
||||
(declare reload-renderer!)
|
||||
(declare request-render-preserving-target)
|
||||
(declare render-pending?)
|
||||
@ -744,7 +745,9 @@
|
||||
(t/write-shape-text spans paragraph text)))))
|
||||
|
||||
(defn set-shape-text-content
|
||||
"This function sets shape text content and returns a stream that loads the needed fonts asynchronously"
|
||||
"This function sets shape text content and returns a stream that loads the needed fonts asynchronously.
|
||||
The editor caches the stored content; WASM lays out the active one, with
|
||||
the CJK attrs of the text language only (see `wm/active-content`)."
|
||||
[shape-id content]
|
||||
|
||||
;; Cache content for text editor sync
|
||||
@ -757,17 +760,28 @@
|
||||
(when (wasm/live?)
|
||||
(h/call wasm/internal-module "_clear_shape_text")
|
||||
|
||||
(set-shape-vertical-align (get content :vertical-align))
|
||||
(set-shape-line-adjustment (get content :line-adjustment))
|
||||
(let [content (wm/active-content content)]
|
||||
(set-shape-vertical-align (get content :vertical-align))
|
||||
(set-shape-line-adjustment (get content :line-adjustment))
|
||||
|
||||
(let [fonts (f/get-content-fonts content)
|
||||
fallback-fonts (fonts-from-text-content content false)
|
||||
all-fonts (concat fonts fallback-fonts)
|
||||
result (f/store-fonts all-fonts)]
|
||||
(write-text-content! content)
|
||||
(f/load-fallback-fonts-for-editor! fallback-fonts)
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result)))
|
||||
(let [fonts (f/get-content-fonts content)
|
||||
fallback-fonts (fonts-from-text-content content false)
|
||||
all-fonts (concat fonts fallback-fonts)
|
||||
result (f/store-fonts all-fonts)]
|
||||
(write-text-content! content)
|
||||
(f/load-fallback-fonts-for-editor! fallback-fonts)
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result))))
|
||||
|
||||
(defn refresh-text-contents
|
||||
"Lay the text `shapes` out again from their stored content, after a change
|
||||
outside the shapes (the file language) changed how they render."
|
||||
[shapes]
|
||||
(when (wasm/live?)
|
||||
(doseq [{:keys [id content]} shapes]
|
||||
(wselect/use-shape id)
|
||||
(set-shape-text-content id (ensure-text-content content)))
|
||||
(request-render "refresh-text-contents")))
|
||||
|
||||
(defn finish-text-editor-composition
|
||||
[text]
|
||||
|
||||
@ -1220,12 +1220,12 @@
|
||||
|
||||
(defn apply-paragraph-attrs-to-range
|
||||
"Apply paragraph attrs to the paragraphs `selection` touches (a caret
|
||||
touches one). Whole-shape attrs (writing mode, orientation) go to every
|
||||
paragraph; a nil value removes them."
|
||||
touches one). Whole-text attrs (writing mode, orientation, language) go
|
||||
to every paragraph; a nil value removes them."
|
||||
[content selection attrs]
|
||||
(let [{:keys [start-para end-para]} (normalize-selection selection)
|
||||
whole-attrs (select-keys attrs cjk/whole-shape-paragraph-attrs)
|
||||
range-attrs (apply dissoc attrs cjk/whole-shape-paragraph-attrs)
|
||||
whole-attrs (select-keys attrs cjk/whole-text-paragraph-attrs)
|
||||
range-attrs (apply dissoc attrs cjk/whole-text-paragraph-attrs)
|
||||
apply-whole (fn [para]
|
||||
(reduce-kv (fn [para attr value]
|
||||
(if (some? value)
|
||||
|
||||
@ -24,11 +24,13 @@
|
||||
|
||||
(defn v2-default-text-content
|
||||
"Build the base text tree (root -> paragraph-set -> paragraph -> span) with the
|
||||
current default typography. Used by the V2 editor/WASM path when a shape is
|
||||
created with no content yet."
|
||||
current default typography and the remembered text language. Used by the V2
|
||||
editor/WASM path when a shape is created with no content yet."
|
||||
[]
|
||||
(let [default-font (deref refs/default-font)
|
||||
text-defaults (merge (txt/get-default-text-attrs) default-font)
|
||||
language (deref refs/default-text-language)
|
||||
text-defaults (cond-> (merge (txt/get-default-text-attrs) default-font)
|
||||
(some? language) (assoc :text-language language))
|
||||
default-span (merge {:text ""}
|
||||
(select-keys text-defaults txt/text-node-attrs))
|
||||
default-paragraph (merge {:type "paragraph"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
(ns app.util.text.geometry
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.text.cjk-layout :as cjk]))
|
||||
[app.util.text.writing-mode :as wm]))
|
||||
|
||||
(defn- live-axis
|
||||
"Measured axis, or the committed one when the measure is missing or not
|
||||
@ -19,7 +19,7 @@
|
||||
"Auto-height grows along the block axis: height horizontally, width vertically."
|
||||
[selrect grow-type content dimension]
|
||||
(when (or (= :fixed grow-type) (some? dimension))
|
||||
(let [vertical? (cjk/vertical-text-content? content)]
|
||||
(let [vertical? (wm/vertical-text-content? content)]
|
||||
{:width (if (or (= :fixed grow-type)
|
||||
(and (= :auto-height grow-type) (not vertical?)))
|
||||
(:width selrect)
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
(defn- block-anchor
|
||||
[content]
|
||||
(if (cjk/vertical-text-content? content)
|
||||
(if (wm/vertical-text-content? content)
|
||||
(case (:vertical-align content) "center" 0.5 "bottom" 0 1)
|
||||
(case (:vertical-align content) "center" 0.5 "bottom" 1 0)))
|
||||
|
||||
@ -39,14 +39,14 @@
|
||||
"Keep the text's block alignment anchored when its measured size changes."
|
||||
[{:keys [points content]}]
|
||||
(gpt/lerp (nth points 0)
|
||||
(nth points (if (cjk/vertical-text-content? content) 1 3))
|
||||
(nth points (if (wm/vertical-text-content? content) 1 3))
|
||||
(block-anchor content)))
|
||||
|
||||
(defn live-rect
|
||||
"Bounds of live editor content, before its geometry is committed."
|
||||
[selrect grow-type content dimension]
|
||||
(if-let [{:keys [width height]} (resolve-size selrect grow-type content dimension)]
|
||||
(let [vertical? (cjk/vertical-text-content? content)
|
||||
(let [vertical? (wm/vertical-text-content? content)
|
||||
anchor (block-anchor content)]
|
||||
(assoc selrect
|
||||
:x (+ (:x selrect) (if vertical? (* anchor (- (:width selrect) width)) 0))
|
||||
|
||||
@ -5,26 +5,68 @@
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.util.text.layout
|
||||
"Language of text shapes, as the CJK text options read it. A text has one
|
||||
language: every paragraph carries the same `:text-language` tag. A text
|
||||
without one takes the file language."
|
||||
(:require
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[clojure.string :as str]))
|
||||
[app.common.types.text :as txt]
|
||||
[app.util.i18n :refer [tr]]))
|
||||
|
||||
(defn controls-enabled?
|
||||
[{:keys [feature-enabled file? all-files?]}]
|
||||
(and (true? feature-enabled)
|
||||
^boolean (wm/vertical-layout-active?)
|
||||
(boolean (or file? all-files?))))
|
||||
(defn- get-paragraph-languages
|
||||
"Language of each paragraph of `content`, `default-language` for the ones
|
||||
without; a content without paragraphs reads as one such paragraph."
|
||||
[content default-language]
|
||||
(let [paragraphs (txt/node-seq txt/is-paragraph-node? content)]
|
||||
(if (seq paragraphs)
|
||||
(map #(or (:text-language %) default-language) paragraphs)
|
||||
[default-language])))
|
||||
|
||||
(defn file-action-visible?
|
||||
[{:keys [feature-enabled all-files?]} can-edit? read-only?]
|
||||
(and (true? feature-enabled)
|
||||
^boolean (wm/vertical-layout-active?)
|
||||
(true? can-edit?)
|
||||
(not read-only?)
|
||||
(not all-files?)))
|
||||
(defn get-texts-language
|
||||
"Language of the text `contents`: the tag all their paragraphs share, nil
|
||||
when none has a language, `:multiple` when they differ. A paragraph
|
||||
without language takes `default-language`, so with none it differs from
|
||||
one with a language."
|
||||
([contents]
|
||||
(get-texts-language contents nil))
|
||||
([contents default-language]
|
||||
(let [languages (into #{} (mapcat #(get-paragraph-languages % default-language)) contents)]
|
||||
(if (> (count languages) 1)
|
||||
:multiple
|
||||
(first languages)))))
|
||||
|
||||
(defn profile-matches?
|
||||
[language text-language]
|
||||
(and (string? text-language)
|
||||
(= (case language :japanese "ja" :chinese "zh" :korean "ko")
|
||||
(first (str/split (str/lower-case text-language) #"-")))))
|
||||
(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))
|
||||
|
||||
(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"))]))
|
||||
|
||||
@ -5,30 +5,56 @@
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns app.util.text.writing-mode
|
||||
"Writing mode of text content as the active renderer lays it out. Only the
|
||||
WASM renderer supports vertical writing; the SVG renderer lays every text
|
||||
out horizontally and leaves the stored attrs as they are."
|
||||
"Text content as the active renderer lays it out. The CJK attrs only apply
|
||||
under the language that uses them: the text language, or the file language
|
||||
for a text without one (see `cjk/active-content`). Only the WASM renderer
|
||||
supports vertical writing; the SVG renderer lays every text out
|
||||
horizontally and leaves the stored attrs as they are."
|
||||
(:require
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.types.text.cjk-layout :as cjk]))
|
||||
[app.common.types.text.cjk-layout :as cjk]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.store :as st]))
|
||||
|
||||
(defn vertical-layout-active?
|
||||
"True when the active renderer lays out vertical writing (WASM)."
|
||||
[]
|
||||
cts/wasm-enabled?)
|
||||
|
||||
(defn get-file-language
|
||||
"Language of the open file for its texts without one, or nil."
|
||||
[]
|
||||
(ctf/get-default-text-language (dsh/lookup-file-data @st/state)))
|
||||
|
||||
(defn active-content
|
||||
"Text content with the CJK attrs its language uses (see
|
||||
`cjk/active-content`)."
|
||||
[content]
|
||||
(cjk/active-content content (get-file-language)))
|
||||
|
||||
(defn content-writing-mode
|
||||
"Effective writing mode of a text content (see `cjk/content-writing-mode`)."
|
||||
"Writing mode the active renderer lays the text content out with."
|
||||
[content]
|
||||
(when ^boolean (vertical-layout-active?)
|
||||
(cjk/content-writing-mode content)))
|
||||
(cjk/active-writing-mode content (get-file-language))))
|
||||
|
||||
(defn vertical-text-content?
|
||||
"True when the text content flows vertically under the active renderer."
|
||||
[content]
|
||||
(and ^boolean (vertical-layout-active?)
|
||||
(cjk/vertical-text-content? content)))
|
||||
(= "vertical-rl" (content-writing-mode content)))
|
||||
|
||||
(defn vertical-values?
|
||||
"True when the text option `values` describe vertical text: vertical
|
||||
writing under a CJK language, the text one or else the file one. Mixed
|
||||
languages count as CJK."
|
||||
[values]
|
||||
(let [language (:text-language values)]
|
||||
(and ^boolean (vertical-layout-active?)
|
||||
(= "vertical-rl" (:writing-mode values))
|
||||
(or (= :multiple language)
|
||||
(some? (cjk/language-family (or language (get-file-language))))))))
|
||||
|
||||
(defn stale-vertical-layout?
|
||||
"True when the content is stored as vertical but the active renderer lays
|
||||
@ -47,9 +73,10 @@
|
||||
(apply dissoc paragraph cjk/whole-shape-paragraph-attrs)))
|
||||
|
||||
(defn renderable-content
|
||||
"Text content as the active renderer lays it out (see
|
||||
"Text content as the active renderer lays it out (see `active-content` and
|
||||
`renderable-paragraph`)."
|
||||
[content]
|
||||
(if ^boolean (vertical-layout-active?)
|
||||
content
|
||||
(txt/transform-nodes txt/is-paragraph-node? renderable-paragraph content)))
|
||||
(let [content (active-content content)]
|
||||
(if ^boolean (vertical-layout-active?)
|
||||
content
|
||||
(txt/transform-nodes txt/is-paragraph-node? renderable-paragraph content))))
|
||||
|
||||
@ -186,7 +186,7 @@
|
||||
(def ^:private ruby-text-content
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set"
|
||||
:children [{:type "paragraph"
|
||||
:children [{:type "paragraph" :text-language "ja"
|
||||
:writing-mode "vertical-rl"
|
||||
:text-orientation "upright"
|
||||
:children [{:text "漢字"
|
||||
@ -224,7 +224,7 @@
|
||||
(def ^:private warichu-text-content
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set"
|
||||
:children [{:type "paragraph"
|
||||
:children [{:type "paragraph" :text-language "ja"
|
||||
:writing-mode "vertical-rl"
|
||||
:text-orientation "upright"
|
||||
:children [{:text "割注入り"
|
||||
@ -236,7 +236,7 @@
|
||||
(def ^:private palt-text-content
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set"
|
||||
:children [{:type "paragraph"
|
||||
:children [{:type "paragraph" :text-language "ja"
|
||||
:children [{:text "かな"
|
||||
:font-features "palt"
|
||||
:font-size "20"
|
||||
@ -267,7 +267,7 @@
|
||||
(def ^:private tcy-digits2-text-content
|
||||
{:type "root"
|
||||
:children [{:type "paragraph-set"
|
||||
:children [{:type "paragraph"
|
||||
:children [{:type "paragraph" :text-language "ja"
|
||||
:writing-mode "vertical-rl"
|
||||
:children [{:text "平成31年"
|
||||
:text-combine-upright "digits2"
|
||||
|
||||
@ -30,15 +30,25 @@
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn- text-content
|
||||
"Japanese text content with `writing-mode`: vertical writing needs a CJK
|
||||
language."
|
||||
[writing-mode]
|
||||
{:children [{:children [{:writing-mode writing-mode}]}]})
|
||||
{:children [{:children [{:writing-mode writing-mode :text-language "ja"}]}]})
|
||||
|
||||
(t/deftest vertical-auto-height-grows-width
|
||||
(let [selrect {:width 100 :height 200}
|
||||
dimension {:width 240 :height 360}
|
||||
content (text-content "vertical-rl")]
|
||||
(t/is (= {:width 240 :height 200}
|
||||
(dwwt/resolve-text-size selrect :auto-height content dimension)))))
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
(let [selrect {:width 100 :height 200}
|
||||
dimension {:width 240 :height 360}
|
||||
content (text-content "vertical-rl")]
|
||||
(t/is (= {:width 240 :height 200}
|
||||
(dwwt/resolve-text-size selrect :auto-height content dimension))))))
|
||||
|
||||
(t/deftest vertical-writing-without-language-grows-as-horizontal
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
(let [content {:children [{:children [{:writing-mode "vertical-rl"}]}]}]
|
||||
(t/is (= {:width 100 :height 360}
|
||||
(dwwt/resolve-text-size {:width 100 :height 200} :auto-height
|
||||
content {:width 240 :height 360}))))))
|
||||
|
||||
(t/deftest horizontal-auto-height-grows-height
|
||||
(let [selrect {:width 100 :height 200}
|
||||
@ -48,28 +58,30 @@
|
||||
(dwwt/resolve-text-size selrect :auto-height content dimension)))))
|
||||
|
||||
(t/deftest vertical-auto-grow-keeps-the-block-anchor
|
||||
(doseq [[align expected-x] [["top" -90] ["center" -40] ["bottom" 10]]]
|
||||
(let [shape (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height
|
||||
:content (assoc (text-content "vertical-rl") :vertical-align align)})
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 200 :height 200})
|
||||
([_ _] {:width 200 :height 200}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (= expected-x (:x resized)) align)
|
||||
(t/is (= 200 (:height resized)) align))))
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
(doseq [[align expected-x] [["top" -90] ["center" -40] ["bottom" 10]]]
|
||||
(let [shape (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height
|
||||
:content (assoc (text-content "vertical-rl") :vertical-align align)})
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 200 :height 200})
|
||||
([_ _] {:width 200 :height 200}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (= expected-x (:x resized)) align)
|
||||
(t/is (= 200 (:height resized)) align)))))
|
||||
|
||||
(t/deftest live-vertical-bounds-grow-left-and-keep-the-wrap-height
|
||||
(let [selrect {:x 10 :y 20 :width 100 :height 200}
|
||||
content (text-content "vertical-rl")]
|
||||
(t/is (= {:x -130 :y 20 :width 240 :height 200}
|
||||
(text.geom/live-rect selrect :auto-height content {:width 240 :height 360})))
|
||||
(t/is (= {:x -130 :y 20 :width 240 :height 360}
|
||||
(text.geom/live-rect selrect :auto-width content {:width 240 :height 360})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :fixed content {:width 240 :height 360})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :auto-width content nil)))))
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
(let [selrect {:x 10 :y 20 :width 100 :height 200}
|
||||
content (text-content "vertical-rl")]
|
||||
(t/is (= {:x -130 :y 20 :width 240 :height 200}
|
||||
(text.geom/live-rect selrect :auto-height content {:width 240 :height 360})))
|
||||
(t/is (= {:x -130 :y 20 :width 240 :height 360}
|
||||
(text.geom/live-rect selrect :auto-width content {:width 240 :height 360})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :fixed content {:width 240 :height 360})))
|
||||
(t/is (= selrect (text.geom/live-rect selrect :auto-width content nil))))))
|
||||
|
||||
(t/deftest live-horizontal-bounds-grow-down-and-keep-the-wrap-width
|
||||
(let [selrect {:x 10 :y 20 :width 100 :height 200}
|
||||
@ -88,16 +100,17 @@
|
||||
(text.geom/live-rect selrect :auto-width content {:width 0 :height 360})))))
|
||||
|
||||
(t/deftest vertical-auto-grow-keeps-the-anchor-after-rotation
|
||||
(let [base (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height :content (text-content "vertical-rl")})
|
||||
shape (gsh/transform-shape base (ctm/rotation-modifiers base (gpt/point 60 120) 45))
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 200 :height 200})
|
||||
([_ _] {:width 200 :height 200}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (gpt/almost-zero? (gpt/subtract (nth (:points shape) 1) (nth (:points resized) 1))))))
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
(let [base (cts/setup-shape
|
||||
{:type :text :x 10 :y 20 :width 100 :height 200
|
||||
:grow-type :auto-height :content (text-content "vertical-rl")})
|
||||
shape (gsh/transform-shape base (ctm/rotation-modifiers base (gpt/point 60 120) 45))
|
||||
resized (with-redefs [dwwt/get-wasm-text-new-size
|
||||
(fn ([_] {:width 200 :height 200})
|
||||
([_ _] {:width 200 :height 200}))]
|
||||
(let [modifiers (dwwt/resize-wasm-text-modifiers shape)]
|
||||
(gsh/transform-shape shape (get-in modifiers [(:id shape) :modifiers]))))]
|
||||
(t/is (gpt/almost-zero? (gpt/subtract (nth (:points shape) 1) (nth (:points resized) 1)))))))
|
||||
|
||||
(t/deftest clicking-vertical-text-anchors-its-right-edge-at-the-click
|
||||
(with-redefs [cts/wasm-enabled? true]
|
||||
|
||||
@ -559,13 +559,13 @@
|
||||
(t/is (not-any? #(contains? % :text-orientation) paragraphs))
|
||||
(t/is (= [nil "center" nil] (mapv :text-align paragraphs)))))
|
||||
|
||||
(t/deftest language-and-wrapping-only-change-selected-paragraphs
|
||||
(t/deftest language-applies-to-the-whole-text-and-wrapping-to-selected-paragraphs
|
||||
(let [content (editor-content [{:text "日本"}] [{:text "한국어"}] [{:text "中文"}])
|
||||
result (text-editor/apply-paragraph-attrs-to-range
|
||||
content (selection 1 0 1 0)
|
||||
{:text-language "ko" :korean-wrap "spaces-only"})
|
||||
paragraphs (-> result :children first :children)]
|
||||
(t/is (= [nil "ko" nil] (mapv :text-language paragraphs)))
|
||||
(t/is (= ["ko" "ko" "ko"] (mapv :text-language paragraphs)))
|
||||
(t/is (= [nil "spaces-only" nil] (mapv :korean-wrap paragraphs)))
|
||||
(t/is (= result
|
||||
(text-editor/merge-exported-texts-into-content
|
||||
|
||||
@ -137,6 +137,7 @@
|
||||
[frontend-tests.ui.ds-controls-numeric-input-test]
|
||||
[frontend-tests.ui.error-boundary-test]
|
||||
[frontend-tests.ui.gradient-handlers-test]
|
||||
[frontend-tests.ui.grouped-dropdown-test]
|
||||
[frontend-tests.ui.history-test]
|
||||
[frontend-tests.ui.inspect-geometry-test]
|
||||
[frontend-tests.ui.inspect-stroke-tokens-test]
|
||||
@ -149,6 +150,7 @@
|
||||
[frontend-tests.ui.routes-test]
|
||||
[frontend-tests.ui.settings-password-schema-test]
|
||||
[frontend-tests.ui.settings-shortcuts-test]
|
||||
[frontend-tests.ui.settings-text-language-test]
|
||||
[frontend-tests.ui.shape-filters-test]
|
||||
[frontend-tests.ui.shortcuts-labels-test]
|
||||
[frontend-tests.ui.sidebar-scroll-test]
|
||||
@ -323,6 +325,7 @@
|
||||
'frontend-tests.ui.ds-controls-numeric-input-test
|
||||
'frontend-tests.ui.error-boundary-test
|
||||
'frontend-tests.ui.gradient-handlers-test
|
||||
'frontend-tests.ui.grouped-dropdown-test
|
||||
'frontend-tests.ui.history-test
|
||||
'frontend-tests.ui.inspect-geometry-test
|
||||
'frontend-tests.ui.inspect-stroke-tokens-test
|
||||
@ -337,6 +340,7 @@
|
||||
'frontend-tests.text-editor-paste-guard-test
|
||||
'frontend-tests.ui.settings-password-schema-test
|
||||
'frontend-tests.ui.settings-shortcuts-test
|
||||
'frontend-tests.ui.settings-text-language-test
|
||||
'frontend-tests.ui.shape-filters-test
|
||||
'frontend-tests.ui.shortcuts-labels-test
|
||||
'frontend-tests.ui.sidebar-scroll-test
|
||||
|
||||
66
frontend/test/frontend_tests/ui/grouped_dropdown_test.cljs
Normal file
66
frontend/test/frontend_tests/ui/grouped_dropdown_test.cljs
Normal file
@ -0,0 +1,66 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.ui.grouped-dropdown-test
|
||||
(:require
|
||||
["react-dom/server" :as rds]
|
||||
[app.main.ui.components.grouped-dropdown :refer [grouped-dropdown*]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.measures :as measures]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private sample-options
|
||||
[{:id "fruits" :label "FRUITS" :header true}
|
||||
{:id "apple" :label "Apple" :detail "red"}
|
||||
{:id "pear" :label "Pear" :selected true}])
|
||||
|
||||
(defn- render
|
||||
[{:keys [show options search-term on-search-change empty-label]
|
||||
:or {show true}}]
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element grouped-dropdown*
|
||||
(mf/props {:show show
|
||||
:on-close identity
|
||||
:on-select identity
|
||||
:options options
|
||||
:search-term search-term
|
||||
:on-search-change on-search-change
|
||||
:empty-label empty-label}))))
|
||||
|
||||
(t/deftest headers-are-not-options
|
||||
(let [markup (render {:options sample-options})]
|
||||
(t/is (= 2 (count (re-seq #"role=\"option\"" markup))))
|
||||
(t/is (re-find #"role=\"presentation\"[^>]*>FRUITS</li>" markup))
|
||||
(t/is (str/includes? markup "red"))
|
||||
(t/is (= ["false" "true"]
|
||||
(mapv second (re-seq #"aria-selected=\"([^\"]+)\"" markup))))))
|
||||
|
||||
(t/deftest search-bar-shows-only-with-a-search-handler
|
||||
(t/is (not (str/includes? (render {:options sample-options}) "<input")))
|
||||
(t/is (str/includes? (render {:options sample-options
|
||||
:search-term ""
|
||||
:on-search-change identity})
|
||||
"<input")))
|
||||
|
||||
(t/deftest empty-options-show-the-empty-label
|
||||
(let [markup (render {:options [] :empty-label "Nothing found"})]
|
||||
(t/is (str/includes? markup "Nothing found"))
|
||||
(t/is (not (str/includes? markup "role=\"option\"")))))
|
||||
|
||||
(t/deftest hidden-dropdown-renders-nothing
|
||||
(t/is (= "" (render {:show false :options sample-options}))))
|
||||
|
||||
(t/deftest size-presets-become-grouped-rows
|
||||
(let [rows (measures/size-preset-options
|
||||
[{:name "APPLE"}
|
||||
{:name "iPhone 16" :width 393 :height 852}
|
||||
{:name "iPhone 16 Pro" :width 402 :height 874}]
|
||||
402 874)]
|
||||
(t/is (= [{:id "APPLE" :label "APPLE" :header true}
|
||||
{:id "iPhone 16" :label "iPhone 16" :detail "393 x 852" :selected false}
|
||||
{:id "iPhone 16 Pro" :label "iPhone 16 Pro" :detail "402 x 874" :selected true}]
|
||||
rows))))
|
||||
@ -0,0 +1,25 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.ui.settings-text-language-test
|
||||
(:require
|
||||
["react-dom/server" :as rds]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.settings.options :as options]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- render
|
||||
[language]
|
||||
(with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element @#'options/text-language-settings* (mf/props {:language language})))))
|
||||
|
||||
(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")))
|
||||
@ -9,6 +9,7 @@
|
||||
["react-dom/server" :as rds]
|
||||
[app.common.types.text.cjk-layout :as cjk]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.controls.select :as select]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.shapes.text.html-text :as html-text]
|
||||
@ -21,6 +22,7 @@
|
||||
[app.util.text.writing-mode :as wm]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private japanese-span-attrs
|
||||
@ -37,110 +39,147 @@
|
||||
:annotation-clearance])
|
||||
|
||||
|
||||
(t/deftest layout-controls-follow-file-or-profile-configuration
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)]
|
||||
(t/is (false? (tl/controls-enabled? {})))
|
||||
(t/is (false? (tl/controls-enabled? {:feature-enabled true :file? false :all-files? false})))
|
||||
(t/is (true? (tl/controls-enabled? {:feature-enabled true :file? true :all-files? false})))
|
||||
(t/is (true? (tl/controls-enabled? {:feature-enabled true :file? false :all-files? true}))))
|
||||
(with-redefs [wm/vertical-layout-active? (constantly false)]
|
||||
(t/is (false? (tl/controls-enabled? {:feature-enabled true :file? true :all-files? true})))))
|
||||
(defn- text-content
|
||||
"Text content with one paragraph per language in `languages`; nil leaves
|
||||
the paragraph without language."
|
||||
[& languages]
|
||||
{:type "root"
|
||||
:children
|
||||
[{:type "paragraph-set"
|
||||
:children
|
||||
(mapv (fn [language]
|
||||
(cond-> {:type "paragraph" :children [{:text "文"}]}
|
||||
(some? language) (assoc :text-language language)))
|
||||
languages)}]})
|
||||
|
||||
(t/deftest language-controls-and-menu-actions-respect-independent-preferences
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)]
|
||||
(doseq [file? [true false] all-files? [true false]]
|
||||
(let [config {:feature-enabled true :file? file? :all-files? all-files?}]
|
||||
(t/is (= (or file? all-files?) (tl/controls-enabled? config)))
|
||||
(t/is (= (not all-files?) (tl/file-action-visible? config true false)))
|
||||
(t/is (false? (tl/file-action-visible? config false false)))
|
||||
(t/is (false? (tl/file-action-visible? config true true)))
|
||||
(t/is (false? (tl/controls-enabled? (assoc config :feature-enabled false)))))))
|
||||
(with-redefs [wm/vertical-layout-active? (constantly false)]
|
||||
(t/is (false? (tl/controls-enabled? {:feature-enabled true :file? true})))
|
||||
(t/is (false? (tl/file-action-visible? {:feature-enabled true :file? true} true false)))))
|
||||
(defn- render-language-section
|
||||
"Static markup of the Language panel for texts that render with `language`
|
||||
and store `own` (the same by default)."
|
||||
([language]
|
||||
(render-language-section language language))
|
||||
([language own]
|
||||
(with-redefs [refs/texts-language (fn [_] (l/atom {:language language :own own}))
|
||||
hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element tll/language-section*
|
||||
(mf/props {:ids [(random-uuid)]
|
||||
:options-props (mf/props {:values {:text-language own}
|
||||
:on-change identity})
|
||||
:on-change identity}))))))
|
||||
|
||||
(t/deftest choosing-a-profile-only-changes-the-paragraph-language
|
||||
(t/is (= {:text-language "ko"} (tll/profile-change "ko")))
|
||||
(t/is (= {:text-language nil} (tll/profile-change tll/plain-profile)))
|
||||
(t/is (tl/profile-matches? :japanese "ja-JP"))
|
||||
(t/is (tl/profile-matches? :chinese "zh-Hant-TW"))
|
||||
(t/is (tl/profile-matches? :korean "KO-kr"))
|
||||
(t/is (not (tl/profile-matches? :chinese "ja")))
|
||||
(t/is (not (tl/profile-matches? :korean :multiple)))
|
||||
(t/is (not (tl/profile-matches? :japanese nil))))
|
||||
(t/deftest texts-without-language-take-the-default-language
|
||||
(t/is (= "ja" (tl/get-texts-language [(text-content nil) (text-content "ja")] "ja")))
|
||||
(t/is (= "ko" (tl/get-texts-language [(text-content nil) nil] "ko")))
|
||||
(t/is (= :multiple (tl/get-texts-language [(text-content nil) (text-content "zh-Hant-TW")] "ja"))))
|
||||
|
||||
(t/deftest profile-choices-keep-mixed-and-other-language-values-visible
|
||||
(let [mixed (last (tll/profile-options :korean :multiple identity))
|
||||
other (last (tll/profile-options :chinese "ja" identity))]
|
||||
(t/is (= {:id "multiple" :label "settings.multiple" :disabled true :dimmed true} mixed))
|
||||
(t/is (= {:id "ja" :label "ja" :disabled true :dimmed true} other))
|
||||
(t/is (= ["plain" "zh-Hans" "zh-Hant" "zh-Hans-CN" "zh-Hant-CN" "zh-Hant-TW" "zh-Hant-HK"]
|
||||
(mapv :id (tll/profile-options :chinese nil identity))))
|
||||
;; The DS select shows "--" for a blank id, so the plain choice needs one.
|
||||
(t/is (not-any? (comp str/blank? :id) (tll/profile-options :korean nil identity)))
|
||||
(t/is (= "plain" (tll/selected-profile nil)))
|
||||
(t/is (= "multiple" (tll/selected-profile :multiple)))
|
||||
(t/is (= "ko" (tll/selected-profile "ko")))))
|
||||
(t/deftest texts-language-is-mixed-when-any-paragraph-differs
|
||||
(t/is (= "ja" (tl/get-texts-language [(text-content "ja" "ja") (text-content "ja")])))
|
||||
(t/is (nil? (tl/get-texts-language [(text-content nil nil)])))
|
||||
(t/is (nil? (tl/get-texts-language [])))
|
||||
(t/is (= :multiple (tl/get-texts-language [(text-content "ja" nil)]))
|
||||
"a paragraph without language differs from one with it")
|
||||
(t/is (= :multiple (tl/get-texts-language [(text-content "ko") (text-content "zh-Hant-TW")])))
|
||||
(t/is (= :multiple (tl/get-texts-language [(text-content "ko") (text-content nil)])))
|
||||
(t/is (= :multiple (tl/get-texts-language [(text-content "ko") nil]))
|
||||
"a text that has no content yet has no language"))
|
||||
|
||||
(t/deftest language-dropdown-groups-the-regional-languages
|
||||
(let [options (tl/language-options "zh-Hant-TW")]
|
||||
(t/is (= ["japanese" "ja" "korean" "ko" "chinese"
|
||||
"zh-Hans-CN" "zh-Hant-CN" "zh-Hant-TW" "zh-Hant-HK"]
|
||||
(mapv :id options)))
|
||||
(t/is (= ["japanese" "korean" "chinese"] (mapv :id (filter :header options))))
|
||||
(t/is (= ["zh-Hant-TW"] (mapv :id (filter :selected options))))
|
||||
(t/is (not-any? :detail options) "the list shows no language tags")
|
||||
(t/is (not-any? :selected (tl/language-options :multiple)))))
|
||||
|
||||
(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 (= "zh-Hans" (tl/language-label "zh-Hans"))
|
||||
"a tag outside the list shows as it is"))
|
||||
|
||||
(t/deftest language-panel-without-language-only-offers-to-add-one
|
||||
(let [markup (render-language-section nil)]
|
||||
(t/is (str/includes? markup "data-testid=\"text-language-add\""))
|
||||
(t/is (not (str/includes? markup "data-testid=\"text-language-select\"")))
|
||||
(t/is (not (str/includes? markup "horizontal-tb-writing-mode"))
|
||||
"writing mode needs a language")))
|
||||
|
||||
(t/deftest language-panel-shows-the-options-of-the-chosen-language
|
||||
(doseq [[language label] [["ja" "workspace.options.text-options.line-adjustment"]
|
||||
["ko" "workspace.options.text-options.korean-wrap"]
|
||||
["zh-Hant-HK" "workspace.options.text-options.text-emphasis"]]]
|
||||
(let [markup (render-language-section language)]
|
||||
(t/is (not (str/includes? markup "data-testid=\"text-language-add\"")))
|
||||
(t/is (str/includes? markup "data-testid=\"text-language-select\""))
|
||||
(t/is (str/includes? markup "data-testid=\"text-language-remove\""))
|
||||
(t/is (str/includes? markup "horizontal-tb-writing-mode"))
|
||||
(t/is (str/includes? markup label)))))
|
||||
|
||||
(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.line-adjustment"))
|
||||
(t/is (not (str/includes? markup "data-testid=\"text-language-remove\"")))
|
||||
(t/is (not (str/includes? markup "data-testid=\"text-language-add\"")))))
|
||||
|
||||
(t/deftest mixed-languages-hide-the-language-options
|
||||
(let [markup (render-language-section :multiple)]
|
||||
(t/is (str/includes? markup "settings.multiple"))
|
||||
(t/is (str/includes? markup "data-testid=\"text-language-remove\""))
|
||||
(t/is (not (str/includes? markup "data-testid=\"text-language-add\"")))
|
||||
(t/is (not (str/includes? markup "horizontal-tb-writing-mode")))))
|
||||
|
||||
(t/deftest korean-word-wrapping-is-distinct-from-the-saved-experiment
|
||||
(t/is (= ["syllable" "keep-words"]
|
||||
(mapv :id (tll/korean-wrap-options nil identity))))
|
||||
(t/is (= ["syllable" "keep-words" "spaces-only"]
|
||||
(mapv :id (tll/korean-wrap-options "spaces-only" identity))))
|
||||
(t/is (= "workspace.options.text-options.profile-chinese-taiwan"
|
||||
(:label (select/get-option (tll/profile-options :chinese "zh-Hant-TW" identity) "zh-Hant-TW"))))
|
||||
(t/is (= {:text-language "zh-Hant-CN"} (tll/profile-change "zh-Hant-CN"))))
|
||||
(mapv :id (tll/korean-wrap-options "spaces-only" identity)))))
|
||||
|
||||
(t/deftest language-specific-controls-require-an-explicit-matching-profile
|
||||
(t/deftest language-controls-follow-the-language
|
||||
(with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(doseq [[language attr label] [[:japanese "ja" "workspace.options.text-options.line-adjustment"]
|
||||
[:korean "ko" "workspace.options.text-options.korean-wrap"]]
|
||||
profile [nil :multiple "zh-Hans" attr]]
|
||||
(doseq [[language label] [[:japanese "workspace.options.text-options.line-adjustment"]
|
||||
[:korean "workspace.options.text-options.korean-wrap"]]
|
||||
shown [:japanese :chinese :korean]]
|
||||
(let [changes (atom [])
|
||||
markup (rds/renderToStaticMarkup
|
||||
(mf/element tll/language-layout-options*
|
||||
#js {:language language
|
||||
:values {:text-language profile}
|
||||
#js {:language shown
|
||||
:values {}
|
||||
:on-change #(swap! changes conj %)}))]
|
||||
(t/is (= (= profile attr) (str/includes? markup label)))
|
||||
(t/is (= (= shown language) (str/includes? markup label)))
|
||||
(t/is (str/includes? markup "horizontal-tb-writing-mode"))
|
||||
(t/is (empty? @changes) "Showing controls must not assign a profile")))))
|
||||
(t/is (empty? @changes) "Showing controls must not edit text")))))
|
||||
|
||||
|
||||
(t/deftest chinese-decoration-controls-require-a-matching-profile
|
||||
(t/deftest chinese-decoration-controls-only-show-for-chinese
|
||||
(with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(doseq [profile [nil :multiple "ja" "ko" "zh-Hans-CN" "zh-Hant-TW"]]
|
||||
(let [changes (atom [])
|
||||
markup (rds/renderToStaticMarkup
|
||||
(doseq [language [:japanese :chinese :korean]]
|
||||
(let [markup (rds/renderToStaticMarkup
|
||||
(mf/element tll/language-layout-options*
|
||||
(mf/props {:language :chinese
|
||||
:values {:text-language profile}
|
||||
:on-change #(swap! changes conj %)})))]
|
||||
(doseq [label ["workspace.options.text-options.text-emphasis"
|
||||
"workspace.options.text-options.emphasis-position"
|
||||
(mf/props {:language language
|
||||
:values {}
|
||||
:on-change identity})))]
|
||||
(doseq [label ["workspace.options.text-options.emphasis-position"
|
||||
"workspace.options.text-options.underline-position"]]
|
||||
(t/is (= (tl/profile-matches? :chinese profile) (str/includes? markup label))))
|
||||
(t/is (empty? @changes))))))
|
||||
(t/is (= (= :chinese language) (str/includes? markup label))))))))
|
||||
|
||||
(t/deftest pinyin-reading-editor-requires-a-chinese-profile-and-text-selection
|
||||
(t/deftest pinyin-reading-editor-requires-a-text-selection
|
||||
(with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(doseq [profile [nil :multiple "ja" "ko" "zh-Hans" "zh-Hant-TW"]
|
||||
selected [false true]]
|
||||
(doseq [selected [false true]]
|
||||
(let [changes (atom [])
|
||||
markup (rds/renderToStaticMarkup
|
||||
(mf/element tll/language-layout-options*
|
||||
(mf/props {:language :chinese
|
||||
:values {:text-language profile :ruby "nǐ hǎo"}
|
||||
:values {:text-language "zh-Hant-TW" :ruby "nǐ hǎo"}
|
||||
:ruby-values {:ruby-hidden true}
|
||||
:text-selection-active selected
|
||||
:reading-selection-active selected
|
||||
:on-change #(swap! changes conj %)
|
||||
:on-ruby-presentation-change #(swap! changes conj %)})))
|
||||
chinese (tl/profile-matches? :chinese profile)]
|
||||
(t/is (= (and chinese selected)
|
||||
:on-ruby-presentation-change #(swap! changes conj %)})))]
|
||||
(t/is (= selected
|
||||
(str/includes? markup "workspace.options.text-options.pinyin-placeholder")))
|
||||
(t/is (= chinese
|
||||
(str/includes? markup "workspace.options.text-options.pinyin-advanced-options")))
|
||||
(t/is (str/includes? markup "workspace.options.text-options.pinyin-advanced-options"))
|
||||
(t/is (not (str/includes? markup "workspace.options.text-options.ruby-placeholder")))
|
||||
(t/is (empty? @changes) "Showing Pinyin controls must not edit text")))))
|
||||
|
||||
@ -368,7 +407,8 @@
|
||||
(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/ruby-side-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 (every? #(cjk/valid-enum-value? :text-combine-upright %)
|
||||
(values (tjl/text-combine-upright-options true identity))))
|
||||
@ -381,6 +421,7 @@
|
||||
[{:type "paragraph-set"
|
||||
:children
|
||||
[{:type "paragraph"
|
||||
:text-language "ja"
|
||||
:writing-mode "vertical-rl"
|
||||
:text-orientation "upright"
|
||||
:children [{:text "縦"}]}]}]}}
|
||||
@ -390,6 +431,37 @@
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)]
|
||||
(t/is (str/includes? (markup) "writing-mode:vertical-rl")))))
|
||||
|
||||
(t/deftest vertical-writing-only-renders-under-a-cjk-language
|
||||
(let [shape {:id (random-uuid) :width 100 :height 100
|
||||
:content {:type "root"
|
||||
:children
|
||||
[{:type "paragraph-set"
|
||||
:children
|
||||
[{:type "paragraph"
|
||||
:writing-mode "vertical-rl"
|
||||
:children [{:text "縦"}]}]}]}}
|
||||
markup #(rds/renderToStaticMarkup (mf/element html-text/text-shape* #js {:shape shape}))]
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)
|
||||
wm/get-file-language (constantly nil)]
|
||||
(t/is (not (str/includes? (markup) "vertical-rl"))
|
||||
"a text without language renders horizontal"))
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)
|
||||
wm/get-file-language (constantly "zh-Hant-TW")]
|
||||
(t/is (str/includes? (markup) "writing-mode:vertical-rl")
|
||||
"a text without language takes the file language"))))
|
||||
|
||||
(t/deftest vertical-controls-need-a-cjk-language
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)
|
||||
wm/get-file-language (constantly nil)]
|
||||
(t/is (true? (wm/vertical-values? {:writing-mode "vertical-rl" :text-language "ko"})))
|
||||
(t/is (true? (wm/vertical-values? {:writing-mode "vertical-rl" :text-language :multiple})))
|
||||
(t/is (false? (wm/vertical-values? {:writing-mode "vertical-rl"})))
|
||||
(t/is (false? (wm/vertical-values? {:writing-mode "vertical-rl" :text-language "en"})))
|
||||
(t/is (false? (wm/vertical-values? {:writing-mode "horizontal-tb" :text-language "ja"}))))
|
||||
(with-redefs [wm/vertical-layout-active? (constantly true)
|
||||
wm/get-file-language (constantly "ja")]
|
||||
(t/is (true? (wm/vertical-values? {:writing-mode "vertical-rl"})))))
|
||||
|
||||
(defn- alignment-markup
|
||||
([component writing-mode wasm-enabled]
|
||||
(alignment-markup component writing-mode wasm-enabled {}))
|
||||
@ -398,7 +470,7 @@
|
||||
(with-redefs [wm/vertical-layout-active? (constantly wasm-enabled)
|
||||
hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element component #js {:values (assoc values :writing-mode writing-mode)
|
||||
(mf/element component #js {:values (merge {:text-language "ja"} values {:writing-mode writing-mode})
|
||||
:on-change identity})))))
|
||||
|
||||
(defn- alignment-labels
|
||||
@ -529,3 +601,45 @@
|
||||
(t/is (str/includes? html "ㄧ"))
|
||||
(t/is (str/includes? html "ˇ"))
|
||||
(t/is (not (str/includes? html "<rt")))))
|
||||
|
||||
(t/deftest positions-show-only-the-sides-of-the-writing-mode
|
||||
(let [labels #(mapv :label (tjl/position-options %1 %2))]
|
||||
(t/is (= ["workspace.options.text-options.position-over"
|
||||
"workspace.options.text-options.position-under"]
|
||||
(labels :ruby-side false)))
|
||||
(t/is (= ["workspace.options.text-options.position-right"
|
||||
"workspace.options.text-options.position-left"]
|
||||
(labels :ruby-side true)))
|
||||
(t/is (= ["auto" "over" "under"] (mapv :id (tjl/position-options :text-emphasis-position false))))
|
||||
(t/is (= ["auto" "right" "left"] (mapv :id (tjl/position-options :text-emphasis-position true))))
|
||||
(t/is (= ["auto" "under"] (mapv :id (tjl/position-options :text-underline-position false))))
|
||||
(t/is (= ["auto" "right" "left"] (mapv :id (tjl/position-options :text-underline-position true))))))
|
||||
|
||||
(t/deftest picking-a-position-keeps-the-side-of-the-other-writing-mode
|
||||
(t/is (= "under" (tjl/position-choice :text-emphasis-position false "under-left")))
|
||||
(t/is (= "left" (tjl/position-choice :text-emphasis-position true "under-left")))
|
||||
(t/is (= "auto" (tjl/position-choice :text-emphasis-position true "auto")))
|
||||
(t/is (= "under-left" (tjl/position-value :text-emphasis-position false "over-left" "under")))
|
||||
(t/is (= "under-right" (tjl/position-value :text-emphasis-position true "under-left" "right")))
|
||||
(t/is (= "over-right" (tjl/position-value :text-emphasis-position false "auto" "over")))
|
||||
(t/is (= "auto" (tjl/position-value :text-emphasis-position false "under-left" "auto")))
|
||||
(t/is (= "under" (tjl/position-choice :text-underline-position false "under-right")))
|
||||
(t/is (= "auto" (tjl/position-choice :text-underline-position false "left")))
|
||||
(t/is (= "right" (tjl/position-choice :text-underline-position true "under-right")))
|
||||
(t/is (= "under-left" (tjl/position-value :text-underline-position false "left" "under")))
|
||||
(t/is (= "left" (tjl/position-value :text-underline-position false "under-left" "auto")))
|
||||
(t/is (= "under-right" (tjl/position-value :text-underline-position true "under" "right")))
|
||||
(t/is (= "under" (tjl/position-value :text-underline-position true "under-left" "auto")))
|
||||
(t/is (= "under" (tjl/position-value :ruby-side true "over" "under")))
|
||||
(t/is (= :multiple (tjl/position-choice :text-emphasis-position false :multiple))))
|
||||
|
||||
(t/deftest distribute-comes-after-the-writing-mode-row
|
||||
(with-redefs [hooks/use-portal-container (fn ([] nil) ([_] nil))]
|
||||
(doseq [language [:japanese :chinese :korean]]
|
||||
(let [markup (rds/renderToStaticMarkup
|
||||
(mf/element tll/language-layout-options*
|
||||
(mf/props {:language language :values {} :on-change identity})))]
|
||||
(t/is (< (str/index-of markup "horizontal-tb-writing-mode")
|
||||
(str/index-of markup "workspace.options.text-options.text-align-distribute"))
|
||||
(name language))))))
|
||||
|
||||
|
||||
@ -863,18 +863,6 @@ msgstr "Invite people"
|
||||
msgid "dashboard.invite-profile-disabled"
|
||||
msgstr "You don't have permission to invite people to this team"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.japanese-layout.description"
|
||||
msgstr "Makes Japanese text layout controls available in every file."
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.japanese-layout.enable-all-files"
|
||||
msgstr "Enable Japanese text layout for all files"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.japanese-layout.title"
|
||||
msgstr "Japanese text layout"
|
||||
|
||||
#: src/app/main/ui/dashboard/organization_team_switch_menus.cljs:235, src/app/main/ui/dashboard/organization_team_switch_menus.cljs:266
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "Leave org"
|
||||
@ -6652,10 +6640,6 @@ msgstr "Radial gradient"
|
||||
msgid "workspace.header.menu.disable-dynamic-alignment"
|
||||
msgstr "Disable dynamic alignment"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.disable-japanese-layout"
|
||||
msgstr "Disable Japanese text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:254
|
||||
msgid "workspace.header.menu.disable-scale-content"
|
||||
msgstr "Disable proportional scale"
|
||||
@ -6680,10 +6664,6 @@ msgstr "Disable WebGL rendering (beta)"
|
||||
msgid "workspace.header.menu.enable-dynamic-alignment"
|
||||
msgstr "Enable dynamic alignment"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.enable-japanese-layout"
|
||||
msgstr "Enable Japanese text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:255
|
||||
msgid "workspace.header.menu.enable-scale-content"
|
||||
msgstr "Enable proportional scale"
|
||||
@ -8322,10 +8302,6 @@ msgstr "Auto width"
|
||||
msgid "workspace.options.text-options.grow-fixed"
|
||||
msgstr "Fixed"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.japanese-layout"
|
||||
msgstr "Japanese text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:688
|
||||
msgid "workspace.options.text-options.letter-spacing"
|
||||
msgstr "Letter Spacing"
|
||||
@ -8336,15 +8312,15 @@ msgstr "Line height"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby"
|
||||
msgstr "Ruby"
|
||||
msgstr "Furigana"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-advanced-options"
|
||||
msgstr "Advanced furigana options"
|
||||
msgstr "Furigana options"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-align"
|
||||
msgstr "Ruby alignment"
|
||||
msgstr "Alignment"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-align-center"
|
||||
@ -8364,11 +8340,11 @@ msgstr "Start"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-hidden"
|
||||
msgstr "Hide furigana"
|
||||
msgstr "Hide"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-overhang"
|
||||
msgstr "Ruby overhang"
|
||||
msgstr "Overhang"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-overhang-auto"
|
||||
@ -8384,19 +8360,11 @@ msgstr "Furigana"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-side"
|
||||
msgstr "Ruby side"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-side-over"
|
||||
msgstr "Over / right"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-side-under"
|
||||
msgstr "Under / left"
|
||||
msgstr "Position"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-size"
|
||||
msgstr "Ruby size"
|
||||
msgstr "Size"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-size-half"
|
||||
@ -10081,66 +10049,10 @@ msgstr "This request has expired, was already used, or could not be completed. S
|
||||
msgid "mcp.oauth.cancelled"
|
||||
msgstr "Connection cancelled"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-layout.title"
|
||||
msgstr "Text layout by language"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-layout.description"
|
||||
msgstr "Choose which text layout controls are available in all files."
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.chinese-layout.enable-all-files"
|
||||
msgstr "Enable Chinese text layout for all files"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.korean-layout.enable-all-files"
|
||||
msgstr "Enable Korean text layout for all files"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.enable-chinese-layout"
|
||||
msgstr "Enable Chinese text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.disable-chinese-layout"
|
||||
msgstr "Disable Chinese text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.enable-korean-layout"
|
||||
msgstr "Enable Korean text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.disable-korean-layout"
|
||||
msgstr "Disable Korean text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.chinese-layout"
|
||||
msgstr "Chinese text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.korean-layout"
|
||||
msgstr "Korean text layout"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-profile"
|
||||
msgstr "Language profile"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-plain"
|
||||
msgstr "No language profile"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-japanese"
|
||||
msgstr "Japanese"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-simplified"
|
||||
msgstr "Simplified Chinese"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-traditional"
|
||||
msgstr "Traditional Chinese"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-korean"
|
||||
msgstr "Korean"
|
||||
@ -10159,11 +10071,11 @@ msgstr "At spaces (experimental)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-mainland-simplified"
|
||||
msgstr "Mainland China · Simplified"
|
||||
msgstr "Mainland China (Simplified)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-mainland-traditional"
|
||||
msgstr "Mainland China · Traditional"
|
||||
msgstr "Mainland China (Traditional)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-taiwan"
|
||||
@ -10189,26 +10101,6 @@ msgstr "nǐ hǎo"
|
||||
msgid "workspace.options.text-options.pinyin-advanced-options"
|
||||
msgstr "Pinyin options"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-hidden"
|
||||
msgstr "Hide Pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-size"
|
||||
msgstr "Pinyin size"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-align"
|
||||
msgstr "Pinyin alignment"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-overhang"
|
||||
msgstr "Pinyin overhang"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-side"
|
||||
msgstr "Pinyin side"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-selection-hint"
|
||||
msgstr "Select characters within one text style to add a reading. Select the whole base to edit an existing reading."
|
||||
@ -10225,33 +10117,17 @@ msgstr "Underline position"
|
||||
msgid "workspace.options.text-options.position-auto"
|
||||
msgstr "Automatic"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-over-right"
|
||||
msgstr "Above / right"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-over-left"
|
||||
msgstr "Above / left"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-under-right"
|
||||
msgstr "Below / right"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-under-left"
|
||||
msgstr "Below / left"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-under"
|
||||
msgstr "Below text"
|
||||
msgstr "Below"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-left"
|
||||
msgstr "Left in vertical"
|
||||
msgstr "Left"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-right"
|
||||
msgstr "Right in vertical"
|
||||
msgstr "Right"
|
||||
|
||||
msgid "workspace.options.text-options.zhuyin"
|
||||
msgstr "Zhuyin"
|
||||
@ -10268,5 +10144,54 @@ msgstr "Select one Han character. Enter 1–3 Zhuyin symbols and an optional ton
|
||||
msgid "workspace.options.text-options.zhuyin-invalid"
|
||||
msgstr "Enter 1–3 Zhuyin symbols with at most one tone. Place a neutral tone before or after the symbols; place other tones at the end."
|
||||
|
||||
msgid "workspace.options.text-options.zhuyin-hidden"
|
||||
msgstr "Hide Zhuyin"
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language"
|
||||
msgstr "Language"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.add-language"
|
||||
msgstr "Add language"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.remove-language"
|
||||
msgstr "Remove language"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-group-japanese"
|
||||
msgstr "Japanese"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-group-korean"
|
||||
msgstr "Korean"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-group-chinese"
|
||||
msgstr "Chinese"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-chinese-variant"
|
||||
msgstr "Chinese · %s"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.option.language"
|
||||
msgstr "Language"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.language-none"
|
||||
msgstr "None"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-language.title"
|
||||
msgstr "Text language"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-language.description"
|
||||
msgstr "New files start with this language for their texts. Each file can change it in File > Language."
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-language.none"
|
||||
msgstr "None"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.position-over"
|
||||
msgstr "Above"
|
||||
|
||||
@ -8411,9 +8411,6 @@ msgstr "Subrayado (%s)"
|
||||
msgid "workspace.options.text-options.zhuyin"
|
||||
msgstr "Zhuyin"
|
||||
|
||||
msgid "workspace.options.text-options.zhuyin-hidden"
|
||||
msgstr "Ocultar zhuyin"
|
||||
|
||||
msgid "workspace.options.text-options.zhuyin-invalid"
|
||||
msgstr ""
|
||||
"Escribe entre 1 y 3 símbolos zhuyin con un solo tono como máximo. Coloca el "
|
||||
@ -9968,66 +9965,10 @@ msgstr "Este cliente puede leer y modificar diseños mediante conexiones MCP est
|
||||
msgid "mcp.oauth.cancelled"
|
||||
msgstr "Conexión cancelada"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-layout.title"
|
||||
msgstr "Disposición del texto por idioma"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-layout.description"
|
||||
msgstr "Elige qué controles de disposición del texto están disponibles en todos los archivos."
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.chinese-layout.enable-all-files"
|
||||
msgstr "Activar la disposición del texto chino en todos los archivos"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.korean-layout.enable-all-files"
|
||||
msgstr "Activar la disposición del texto coreano en todos los archivos"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.enable-chinese-layout"
|
||||
msgstr "Activar la disposición del texto chino"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.disable-chinese-layout"
|
||||
msgstr "Desactivar la disposición del texto chino"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.enable-korean-layout"
|
||||
msgstr "Activar la disposición del texto coreano"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.disable-korean-layout"
|
||||
msgstr "Desactivar la disposición del texto coreano"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.chinese-layout"
|
||||
msgstr "Disposición del texto chino"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.korean-layout"
|
||||
msgstr "Disposición del texto coreano"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-profile"
|
||||
msgstr "Perfil de idioma"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-plain"
|
||||
msgstr "Sin perfil de idioma"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-japanese"
|
||||
msgstr "Japonés"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-simplified"
|
||||
msgstr "Chino simplificado"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-traditional"
|
||||
msgstr "Chino tradicional"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-korean"
|
||||
msgstr "Coreano"
|
||||
@ -10046,11 +9987,11 @@ msgstr "En los espacios (experimental)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-mainland-simplified"
|
||||
msgstr "China continental · Simplificado"
|
||||
msgstr "China continental (simplificado)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-mainland-traditional"
|
||||
msgstr "China continental · Tradicional"
|
||||
msgstr "China continental (tradicional)"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.profile-chinese-taiwan"
|
||||
@ -10076,26 +10017,6 @@ msgstr "nǐ hǎo"
|
||||
msgid "workspace.options.text-options.pinyin-advanced-options"
|
||||
msgstr "Opciones de pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-hidden"
|
||||
msgstr "Ocultar pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-size"
|
||||
msgstr "Tamaño del pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-align"
|
||||
msgstr "Alineación del pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-overhang"
|
||||
msgstr "Saliente del pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-side"
|
||||
msgstr "Lado del pinyin"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.pinyin-selection-hint"
|
||||
msgstr "Selecciona caracteres con un mismo estilo para añadir una lectura. Selecciona toda la base para editar su lectura."
|
||||
@ -10112,30 +10033,94 @@ msgstr "Posición del subrayado"
|
||||
msgid "workspace.options.text-options.position-auto"
|
||||
msgstr "Automática"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-over-right"
|
||||
msgstr "Arriba / derecha"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-over-left"
|
||||
msgstr "Arriba / izquierda"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-under-right"
|
||||
msgstr "Abajo / derecha"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-under-left"
|
||||
msgstr "Abajo / izquierda"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-under"
|
||||
msgstr "Debajo del texto"
|
||||
msgstr "Debajo"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-left"
|
||||
msgstr "Izquierda en vertical"
|
||||
msgstr "Izquierda"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.position-right"
|
||||
msgstr "Derecha en vertical"
|
||||
msgstr "Derecha"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.add-language"
|
||||
msgstr "Añadir idioma"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.remove-language"
|
||||
msgstr "Quitar idioma"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-group-japanese"
|
||||
msgstr "Japonés"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-group-korean"
|
||||
msgstr "Coreano"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-group-chinese"
|
||||
msgstr "Chino"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_language_layout.cljs
|
||||
msgid "workspace.options.text-options.language-chinese-variant"
|
||||
msgstr "Chino · %s"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.option.language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.language-none"
|
||||
msgstr "Ninguno"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-language.title"
|
||||
msgstr "Idioma de los textos"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-language.description"
|
||||
msgstr "Los archivos nuevos empiezan con este idioma para sus textos. Cada archivo puede cambiarlo en Archivo > Idioma."
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.text-language.none"
|
||||
msgstr "Ninguno"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby"
|
||||
msgstr "Furigana"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-advanced-options"
|
||||
msgstr "Opciones de furigana"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-hidden"
|
||||
msgstr "Ocultar"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-size"
|
||||
msgstr "Tamaño"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-align"
|
||||
msgstr "Alineación"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-overhang"
|
||||
msgstr "Saliente"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-side"
|
||||
msgstr "Posición"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.position-over"
|
||||
msgstr "Encima"
|
||||
|
||||
@ -777,18 +777,6 @@ msgstr "メンバーを招待"
|
||||
msgid "dashboard.invite-profile-disabled"
|
||||
msgstr "このチームにユーザーを招待する権限がありません"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.japanese-layout.description"
|
||||
msgstr "すべてのファイルで日本語組版コントロールを利用できるようにします。"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.japanese-layout.enable-all-files"
|
||||
msgstr "すべてのファイルで日本語組版を有効にする"
|
||||
|
||||
#: src/app/main/ui/settings/options.cljs
|
||||
msgid "dashboard.japanese-layout.title"
|
||||
msgstr "日本語組版"
|
||||
|
||||
#: src/app/main/ui/dashboard/organization_team_switch_menus.cljs:235, src/app/main/ui/dashboard/organization_team_switch_menus.cljs:266
|
||||
msgid "dashboard.leave-organization"
|
||||
msgstr "組織から退出"
|
||||
@ -6172,10 +6160,6 @@ msgstr "放射状グラデーション"
|
||||
msgid "workspace.header.menu.disable-dynamic-alignment"
|
||||
msgstr "動的整列を無効化"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.disable-japanese-layout"
|
||||
msgstr "日本語組版を無効にする"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:254
|
||||
msgid "workspace.header.menu.disable-scale-content"
|
||||
msgstr "比率を保った拡大縮小を無効化"
|
||||
@ -6200,10 +6184,6 @@ msgstr "WebGL レンダリングを無効化(ベータ)"
|
||||
msgid "workspace.header.menu.enable-dynamic-alignment"
|
||||
msgstr "動的整列を有効化"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs
|
||||
msgid "workspace.header.menu.enable-japanese-layout"
|
||||
msgstr "日本語組版を有効にする"
|
||||
|
||||
#: src/app/main/ui/workspace/main_menu.cljs:255
|
||||
msgid "workspace.header.menu.enable-scale-content"
|
||||
msgstr "比率を保った拡大縮小を有効化"
|
||||
@ -7764,10 +7744,6 @@ msgstr "幅の自動調整"
|
||||
msgid "workspace.options.text-options.grow-fixed"
|
||||
msgstr "固定"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.japanese-layout"
|
||||
msgstr "日本語組版"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:688
|
||||
msgid "workspace.options.text-options.letter-spacing"
|
||||
msgstr "文字間隔"
|
||||
@ -7778,7 +7754,7 @@ msgstr "行の高さ"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby"
|
||||
msgstr "ルビ"
|
||||
msgstr "ふりがな"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-advanced-options"
|
||||
@ -7786,7 +7762,7 @@ msgstr "ふりがなの詳細設定"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-align"
|
||||
msgstr "ルビの配置"
|
||||
msgstr "配置"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-align-center"
|
||||
@ -7806,11 +7782,11 @@ msgstr "先頭"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs
|
||||
msgid "workspace.options.text-options.ruby-hidden"
|
||||
msgstr "ふりがなを非表示"
|
||||
msgstr "非表示"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-overhang"
|
||||
msgstr "ルビのはみ出し"
|
||||
msgstr "はみ出し"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-overhang-auto"
|
||||
@ -7826,19 +7802,11 @@ msgstr "ふりがな"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-side"
|
||||
msgstr "ルビの側"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-side-over"
|
||||
msgstr "上/右"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-side-under"
|
||||
msgstr "下/左"
|
||||
msgstr "位置"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-size"
|
||||
msgstr "ルビの大きさ"
|
||||
msgstr "大きさ"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/text.cljs
|
||||
msgid "workspace.options.text-options.ruby-size-half"
|
||||
|
||||
@ -2424,3 +2424,47 @@ fn spread_lines_start_and_end_at_the_box_edges() {
|
||||
first.rect.left
|
||||
);
|
||||
}
|
||||
|
||||
/// Japanese text distributes like the other languages (均等割り付け).
|
||||
#[test]
|
||||
fn japanese_distribution_reaches_the_box_edges() {
|
||||
use skia::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||
init_state();
|
||||
let mut resources =
|
||||
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||
for text in ["日本", "にほんご", "東京都"] {
|
||||
let mut paragraph = Paragraph::new(
|
||||
TextAlign::Distribute,
|
||||
TextDirection::LTR,
|
||||
None,
|
||||
None,
|
||||
1.0,
|
||||
0.0,
|
||||
vec![TextSpan {
|
||||
fills: vec![crate::shapes::Fill::Solid(crate::shapes::SolidColor(
|
||||
skia::Color::BLACK,
|
||||
))],
|
||||
..make_span(text, 0.0)
|
||||
}],
|
||||
);
|
||||
paragraph.set_language("ja".to_string());
|
||||
let bounds = crate::math::Rect::from_xywh(0.0, 0.0, 200.0, 200.0);
|
||||
let mut content = TextContent::new(bounds, crate::shapes::GrowType::Fixed);
|
||||
content.add_paragraph(paragraph);
|
||||
content.update_layout(bounds);
|
||||
let layout = &content.layout.paragraphs[0][0];
|
||||
let line = &layout.get_line_metrics()[0];
|
||||
let last = layout.get_rects_for_range(
|
||||
line.end_excluding_whitespaces - 1..line.end_excluding_whitespaces,
|
||||
RectHeightStyle::Tight,
|
||||
RectWidthStyle::Tight,
|
||||
)[0]
|
||||
.rect;
|
||||
assert!(
|
||||
(last.right - 200.0).abs() < 0.1,
|
||||
"{text}: ends at {}",
|
||||
last.right
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user