✨ 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:
alonso.torres 2026-11-05 13:19:01 +01:00
parent 9bc8cae3c9
commit 5d647b2cd5
47 changed files with 1790 additions and 1005 deletions

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)))

View File

@ -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)

View File

@ -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")))

View File

@ -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"

View File

@ -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

View File

@ -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]

View File

@ -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))))))))

View File

@ -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

View 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}])))]]])

View 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);
}
}
}

View File

@ -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}])]]))

View File

@ -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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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}])]))

View File

@ -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]

View File

@ -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})])])]))

View File

@ -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;

View File

@ -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]

View File

@ -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)

View File

@ -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"

View File

@ -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))

View File

@ -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"))]))

View File

@ -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))))

View File

@ -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"

View File

@ -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]

View File

@ -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

View File

@ -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

View 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))))

View File

@ -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")))

View File

@ -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))))))

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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
);
}
}