mirror of
https://github.com/penpot/penpot.git
synced 2026-07-28 17:06:21 +00:00
🐛 Fix caret not mimicking text color (#10866)
This commit is contained in:
parent
cb21f6401a
commit
704cd40182
@ -440,6 +440,8 @@
|
||||
(when (= ev TEXT_EDITOR_EVENT_STYLES_CHANGED)
|
||||
(let [current-styles (text-editor/text-editor-get-current-styles)
|
||||
shape-id (text-editor/text-editor-get-active-shape-id)]
|
||||
;; Keep the caret color matching the text at the caret position.
|
||||
(text-editor/text-editor-apply-caret-color (:fills current-styles))
|
||||
(st/emit! (texts/v3-update-text-editor-styles shape-id current-styles))))
|
||||
(recur (or needs-render?
|
||||
(= ev TEXT_EDITOR_EVENT_CONTENT_CHANGED)
|
||||
@ -531,9 +533,13 @@
|
||||
(not (render-pending?)))
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink (js/performance.now))
|
||||
(text-editor/text-editor-render-caret)
|
||||
(when (drain-text-editor-events!)
|
||||
(request-render-preserving-target "text-editor-content")))))
|
||||
;; Drain before painting so the caret color (updated on StylesChanged when
|
||||
;; the caret crosses into differently-colored text) is applied before the
|
||||
;; overlay is drawn, instead of lagging a frame behind.
|
||||
(let [needs-render? (drain-text-editor-events!)]
|
||||
(text-editor/text-editor-render-caret)
|
||||
(when needs-render?
|
||||
(request-render-preserving-target "text-editor-content"))))))
|
||||
|
||||
;; CSS-pixel blur radius for the page-transition snapshot (DPR-scaled in WASM).
|
||||
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
||||
|
||||
@ -128,7 +128,6 @@
|
||||
nil)))
|
||||
|
||||
(def ^:private selection-color-css-var "--text-editor-selection-background-color")
|
||||
(def ^:private caret-color-css-var "--text-editor-caret-color")
|
||||
|
||||
(defn- resolve-theme-color
|
||||
"Resolve a themed CSS color variable (read from the document body) into a
|
||||
@ -140,17 +139,54 @@
|
||||
(dom/get-css-variable css-var js/document.body))]
|
||||
(sr-clr/hex->u32argb color opacity)))
|
||||
|
||||
;; ARGB u32 for opaque white, painted with a Difference blend mode so the caret
|
||||
;; always shows the inverted color of the background.
|
||||
(def ^:private caret-invert-color 0xffffffff)
|
||||
|
||||
(defn text-editor-apply-theme
|
||||
"Push the current theme's selection and caret colors (read from the CSS
|
||||
custom properties on the document body) into the WASM text editor. The
|
||||
editor theme is a persistent singleton, so call once after init and again
|
||||
on every color-scheme change."
|
||||
"Push the current theme's selection color (read from the CSS custom properties
|
||||
on the document body) into the WASM text editor, together with the default
|
||||
caret: white with invert, so it shows the inverted color of the background.
|
||||
The caret only switches to a solid text color (invert off) via
|
||||
`text-editor-apply-caret-color`. The editor theme is a persistent singleton,
|
||||
so call once after init and again on every color-scheme change."
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(let [selection (resolve-theme-color selection-color-css-var)]
|
||||
(when selection
|
||||
(h/call wasm/internal-module "_text_editor_apply_theme" selection caret-invert-color true)))))
|
||||
|
||||
(defn- solid-fill?
|
||||
[fill]
|
||||
(some? (:fill-color fill)))
|
||||
|
||||
(defn resolve-caret-color
|
||||
"Compute the caret color from the text `fills` at the caret (as returned by
|
||||
`text-editor-get-current-styles`), as `{:color <argb-u32> :invert? bool}`:
|
||||
|
||||
- when there is at least one solid fill, match the topmost (visible) one,
|
||||
painted normally (`:invert? false`);
|
||||
- otherwise (no fill, gradient, image fills, mixed selection, …) use white
|
||||
with `:invert? true`, which the renderer paints with a Difference blend so
|
||||
the caret is the inverted color of whatever is behind it."
|
||||
[fills]
|
||||
(if-let [solid (and (sequential? fills)
|
||||
(some #(when (solid-fill? %) %) fills))]
|
||||
{:color (sr-clr/hex->u32argb (:fill-color solid) (:fill-opacity solid))
|
||||
:invert? false}
|
||||
{:color caret-invert-color
|
||||
:invert? true}))
|
||||
|
||||
(defn text-editor-apply-caret-color
|
||||
"Update the WASM text-editor caret color so it matches the text at the caret
|
||||
(see `resolve-caret-color`). Re-applies the current theme selection color
|
||||
unchanged, since the WASM theme is a singleton holding both."
|
||||
[fills]
|
||||
(when wasm/context-initialized?
|
||||
(let [selection (resolve-theme-color selection-color-css-var)
|
||||
caret (resolve-theme-color caret-color-css-var)]
|
||||
(when (and selection caret)
|
||||
(h/call wasm/internal-module "_text_editor_apply_theme" selection caret)))))
|
||||
{:keys [color invert?]} (resolve-caret-color fills)]
|
||||
(when selection
|
||||
(h/call wasm/internal-module "_text_editor_apply_theme" selection color invert?)))))
|
||||
|
||||
(defn text-editor-focus
|
||||
[id]
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns frontend-tests.render-wasm.text-editor-caret-color-test
|
||||
"Unit tests for the text-editor caret color resolution.
|
||||
|
||||
The caret matches the topmost solid fill of the text at the caret. For
|
||||
anything else (no fill, gradient, image fills, mixed selection) it falls back
|
||||
to an inverted caret (white painted with a Difference blend)."
|
||||
(:require
|
||||
[app.render-wasm.serializers.color :as sr-clr]
|
||||
[app.render-wasm.text-editor :as text-editor]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(def ^:private white 0xffffffff)
|
||||
|
||||
(defn- solid [color opacity]
|
||||
{:fill-color color :fill-opacity opacity})
|
||||
|
||||
(defn- gradient [stops]
|
||||
{:fill-color-gradient {:type :linear :stops stops}})
|
||||
|
||||
(t/deftest resolve-caret-color-solid
|
||||
(t/testing "a single solid fill is matched, painted normally"
|
||||
(t/is (= {:color (sr-clr/hex->u32argb "#ff0000" 1) :invert? false}
|
||||
(text-editor/resolve-caret-color [(solid "#ff0000" 1)]))))
|
||||
|
||||
(t/testing "the topmost (first, visible) solid fill wins over the ones below"
|
||||
(t/is (= {:color (sr-clr/hex->u32argb "#ff0000" 1) :invert? false}
|
||||
(text-editor/resolve-caret-color [(solid "#ff0000" 1) (solid "#00ff00" 1)]))))
|
||||
|
||||
(t/testing "the solid fill opacity is preserved"
|
||||
(t/is (= {:color (sr-clr/hex->u32argb "#00ff00" 0.5) :invert? false}
|
||||
(text-editor/resolve-caret-color [{:fill-image {:id "x"}} (solid "#00ff00" 0.5)]))))
|
||||
|
||||
(t/testing "a solid fill takes precedence over a gradient"
|
||||
(t/is (= {:color (sr-clr/hex->u32argb "#ff0000" 1) :invert? false}
|
||||
(text-editor/resolve-caret-color
|
||||
[(solid "#ff0000" 1)
|
||||
(gradient [{:color "#000000" :opacity 1 :offset 0}
|
||||
{:color "#ffffff" :opacity 1 :offset 1}])])))))
|
||||
|
||||
(t/deftest resolve-caret-color-inverted
|
||||
(t/testing "a gradient falls back to the inverted caret"
|
||||
(t/is (= {:color white :invert? true}
|
||||
(text-editor/resolve-caret-color
|
||||
[(gradient [{:color "#ff0000" :opacity 1 :offset 0}
|
||||
{:color "#0000ff" :opacity 1 :offset 1}])]))))
|
||||
|
||||
(t/testing "an image-only fill falls back to the inverted caret"
|
||||
(t/is (= {:color white :invert? true}
|
||||
(text-editor/resolve-caret-color [{:fill-image {:id "x"}}]))))
|
||||
|
||||
(t/testing "no fills fall back to the inverted caret"
|
||||
(t/is (= {:color white :invert? true}
|
||||
(text-editor/resolve-caret-color []))))
|
||||
|
||||
(t/testing "nil falls back to the inverted caret"
|
||||
(t/is (= {:color white :invert? true}
|
||||
(text-editor/resolve-caret-color nil))))
|
||||
|
||||
(t/testing "a mixed selection (:multiple) falls back to the inverted caret"
|
||||
(t/is (= {:color white :invert? true}
|
||||
(text-editor/resolve-caret-color :multiple)))))
|
||||
@ -49,6 +49,7 @@
|
||||
[frontend-tests.plugins.utils-test]
|
||||
[frontend-tests.plugins.value-objects-test]
|
||||
[frontend-tests.render-wasm.process-objects-test]
|
||||
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
||||
[frontend-tests.svg-fills-test]
|
||||
[frontend-tests.text-editor-paste-guard-test]
|
||||
[frontend-tests.tokens.copy-paste-props-test]
|
||||
@ -136,6 +137,7 @@
|
||||
'frontend-tests.plugins.utils-test
|
||||
'frontend-tests.plugins.value-objects-test
|
||||
'frontend-tests.render-wasm.process-objects-test
|
||||
'frontend-tests.render-wasm.text-editor-caret-color-test
|
||||
'frontend-tests.svg-fills-test
|
||||
'frontend-tests.tokens.copy-paste-props-test
|
||||
'frontend-tests.tokens.import-export-test
|
||||
|
||||
@ -66,6 +66,11 @@ fn render_cursor(
|
||||
if editor_state.is_overtype_mode {
|
||||
paint.set_blend_mode(BlendMode::Exclusion);
|
||||
paint.set_color(Color::WHITE);
|
||||
} else if editor_state.theme.cursor_invert {
|
||||
// Default (no solid fill to match): a white caret with a Difference
|
||||
// blend renders the inverted color of whatever is behind it.
|
||||
paint.set_blend_mode(BlendMode::Difference);
|
||||
paint.set_color(editor_state.theme.cursor_color);
|
||||
} else {
|
||||
paint.set_blend_mode(BlendMode::SrcOver);
|
||||
paint.set_color(editor_state.theme.cursor_color);
|
||||
|
||||
@ -101,7 +101,7 @@ pub enum TextEditorEvent {
|
||||
|
||||
/// FIXME: It should be better to get these constants from the frontend through the API.
|
||||
const SELECTION_COLOR: Color = Color::from_argb(127, 0, 209, 184);
|
||||
const CURSOR_COLOR: Color = Color::BLACK;
|
||||
const CURSOR_COLOR: Color = Color::WHITE;
|
||||
const CURSOR_WIDTH: f32 = 1.0;
|
||||
const CURSOR_BLINK_INTERVAL_MS: f32 = 530.0;
|
||||
|
||||
@ -274,6 +274,10 @@ pub struct TextEditorTheme {
|
||||
pub selection_color: Color,
|
||||
pub cursor_color: Color,
|
||||
pub cursor_width: f32,
|
||||
/// When true the caret is painted with a Difference blend mode, so it shows
|
||||
/// as the inverted color of whatever is behind it. Used as the default when
|
||||
/// the text has no solid fill whose color the caret can match.
|
||||
pub cursor_invert: bool,
|
||||
}
|
||||
|
||||
pub struct TextComposition {
|
||||
@ -364,6 +368,7 @@ impl TextEditorState {
|
||||
selection_color: SELECTION_COLOR,
|
||||
cursor_color: CURSOR_COLOR,
|
||||
cursor_width: CURSOR_WIDTH,
|
||||
cursor_invert: true,
|
||||
},
|
||||
selection: TextSelection::new(),
|
||||
composition: TextComposition::new(),
|
||||
|
||||
@ -33,12 +33,17 @@ pub enum CursorDirection {
|
||||
// STATE MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
/// Apply the editor theme. When `invert` is true the caret is painted with a
|
||||
/// Difference blend mode (pass white as `cursor_color` to always show the
|
||||
/// inverted color of the background); otherwise it is painted with the given
|
||||
/// solid `cursor_color`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_apply_theme(selection_color: u32, cursor_color: u32) {
|
||||
pub extern "C" fn text_editor_apply_theme(selection_color: u32, cursor_color: u32, invert: bool) {
|
||||
// NOTE: In the future could be interesting to fill al this data from
|
||||
// a structure pointer.
|
||||
get_text_editor_state().theme.selection_color = Color::new(selection_color);
|
||||
get_text_editor_state().theme.cursor_color = Color::new(cursor_color);
|
||||
get_text_editor_state().theme.cursor_invert = invert;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user