mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 04:48:39 +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)
|
(when (= ev TEXT_EDITOR_EVENT_STYLES_CHANGED)
|
||||||
(let [current-styles (text-editor/text-editor-get-current-styles)
|
(let [current-styles (text-editor/text-editor-get-current-styles)
|
||||||
shape-id (text-editor/text-editor-get-active-shape-id)]
|
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))))
|
(st/emit! (texts/v3-update-text-editor-styles shape-id current-styles))))
|
||||||
(recur (or needs-render?
|
(recur (or needs-render?
|
||||||
(= ev TEXT_EDITOR_EVENT_CONTENT_CHANGED)
|
(= ev TEXT_EDITOR_EVENT_CONTENT_CHANGED)
|
||||||
@ -531,9 +533,13 @@
|
|||||||
(not (render-pending?)))
|
(not (render-pending?)))
|
||||||
(when (is-text-editor-wasm-enabled @st/state)
|
(when (is-text-editor-wasm-enabled @st/state)
|
||||||
(text-editor/text-editor-update-blink (js/performance.now))
|
(text-editor/text-editor-update-blink (js/performance.now))
|
||||||
(text-editor/text-editor-render-caret)
|
;; Drain before painting so the caret color (updated on StylesChanged when
|
||||||
(when (drain-text-editor-events!)
|
;; the caret crosses into differently-colored text) is applied before the
|
||||||
(request-render-preserving-target "text-editor-content")))))
|
;; 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).
|
;; CSS-pixel blur radius for the page-transition snapshot (DPR-scaled in WASM).
|
||||||
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
(def ^:private TRANSITION_BLUR_RADIUS 4.0)
|
||||||
|
|||||||
@ -128,7 +128,6 @@
|
|||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(def ^:private selection-color-css-var "--text-editor-selection-background-color")
|
(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
|
(defn- resolve-theme-color
|
||||||
"Resolve a themed CSS color variable (read from the document body) into a
|
"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))]
|
(dom/get-css-variable css-var js/document.body))]
|
||||||
(sr-clr/hex->u32argb color opacity)))
|
(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
|
(defn text-editor-apply-theme
|
||||||
"Push the current theme's selection and caret colors (read from the CSS
|
"Push the current theme's selection color (read from the CSS custom properties
|
||||||
custom properties on the document body) into the WASM text editor. The
|
on the document body) into the WASM text editor, together with the default
|
||||||
editor theme is a persistent singleton, so call once after init and again
|
caret: white with invert, so it shows the inverted color of the background.
|
||||||
on every color-scheme change."
|
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?
|
(when wasm/context-initialized?
|
||||||
(let [selection (resolve-theme-color selection-color-css-var)
|
(let [selection (resolve-theme-color selection-color-css-var)
|
||||||
caret (resolve-theme-color caret-color-css-var)]
|
{:keys [color invert?]} (resolve-caret-color fills)]
|
||||||
(when (and selection caret)
|
(when selection
|
||||||
(h/call wasm/internal-module "_text_editor_apply_theme" selection caret)))))
|
(h/call wasm/internal-module "_text_editor_apply_theme" selection color invert?)))))
|
||||||
|
|
||||||
(defn text-editor-focus
|
(defn text-editor-focus
|
||||||
[id]
|
[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.utils-test]
|
||||||
[frontend-tests.plugins.value-objects-test]
|
[frontend-tests.plugins.value-objects-test]
|
||||||
[frontend-tests.render-wasm.process-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.svg-fills-test]
|
||||||
[frontend-tests.text-editor-paste-guard-test]
|
[frontend-tests.text-editor-paste-guard-test]
|
||||||
[frontend-tests.tokens.copy-paste-props-test]
|
[frontend-tests.tokens.copy-paste-props-test]
|
||||||
@ -136,6 +137,7 @@
|
|||||||
'frontend-tests.plugins.utils-test
|
'frontend-tests.plugins.utils-test
|
||||||
'frontend-tests.plugins.value-objects-test
|
'frontend-tests.plugins.value-objects-test
|
||||||
'frontend-tests.render-wasm.process-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.svg-fills-test
|
||||||
'frontend-tests.tokens.copy-paste-props-test
|
'frontend-tests.tokens.copy-paste-props-test
|
||||||
'frontend-tests.tokens.import-export-test
|
'frontend-tests.tokens.import-export-test
|
||||||
|
|||||||
@ -66,6 +66,11 @@ fn render_cursor(
|
|||||||
if editor_state.is_overtype_mode {
|
if editor_state.is_overtype_mode {
|
||||||
paint.set_blend_mode(BlendMode::Exclusion);
|
paint.set_blend_mode(BlendMode::Exclusion);
|
||||||
paint.set_color(Color::WHITE);
|
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 {
|
} else {
|
||||||
paint.set_blend_mode(BlendMode::SrcOver);
|
paint.set_blend_mode(BlendMode::SrcOver);
|
||||||
paint.set_color(editor_state.theme.cursor_color);
|
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.
|
/// 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 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_WIDTH: f32 = 1.0;
|
||||||
const CURSOR_BLINK_INTERVAL_MS: f32 = 530.0;
|
const CURSOR_BLINK_INTERVAL_MS: f32 = 530.0;
|
||||||
|
|
||||||
@ -274,6 +274,10 @@ pub struct TextEditorTheme {
|
|||||||
pub selection_color: Color,
|
pub selection_color: Color,
|
||||||
pub cursor_color: Color,
|
pub cursor_color: Color,
|
||||||
pub cursor_width: f32,
|
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 {
|
pub struct TextComposition {
|
||||||
@ -364,6 +368,7 @@ impl TextEditorState {
|
|||||||
selection_color: SELECTION_COLOR,
|
selection_color: SELECTION_COLOR,
|
||||||
cursor_color: CURSOR_COLOR,
|
cursor_color: CURSOR_COLOR,
|
||||||
cursor_width: CURSOR_WIDTH,
|
cursor_width: CURSOR_WIDTH,
|
||||||
|
cursor_invert: true,
|
||||||
},
|
},
|
||||||
selection: TextSelection::new(),
|
selection: TextSelection::new(),
|
||||||
composition: TextComposition::new(),
|
composition: TextComposition::new(),
|
||||||
|
|||||||
@ -33,12 +33,17 @@ pub enum CursorDirection {
|
|||||||
// STATE MANAGEMENT
|
// 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]
|
#[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
|
// NOTE: In the future could be interesting to fill al this data from
|
||||||
// a structure pointer.
|
// a structure pointer.
|
||||||
get_text_editor_state().theme.selection_color = Color::new(selection_color);
|
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_color = Color::new(cursor_color);
|
||||||
|
get_text_editor_state().theme.cursor_invert = invert;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user