mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 12:58:55 +00:00
🔧 Improve text editor selection and tab conversion
This commit is contained in:
parent
6df045b194
commit
79c94eef29
@ -975,7 +975,14 @@
|
|||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [text-editor-instance (:workspace-editor state)
|
(let [text-editor-instance (:workspace-editor state)
|
||||||
objects (dsh/lookup-page-objects state)
|
objects (dsh/lookup-page-objects state)
|
||||||
text-ids (resolve-text-ids objects id)]
|
text-ids (resolve-text-ids objects id)
|
||||||
|
|
||||||
|
wasm-editing?
|
||||||
|
(and (features/active-feature? state "text-editor-wasm/v1")
|
||||||
|
(= id (wasm.api/text-editor-get-active-shape-id)))
|
||||||
|
|
||||||
|
wasm-editing-selection?
|
||||||
|
(and wasm-editing? (wasm.api/text-editor-has-selection?))]
|
||||||
(if (and (features/active-feature? state "text-editor/v2")
|
(if (and (features/active-feature? state "text-editor/v2")
|
||||||
(some? text-editor-instance))
|
(some? text-editor-instance))
|
||||||
(rx/empty)
|
(rx/empty)
|
||||||
@ -985,15 +992,30 @@
|
|||||||
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
||||||
(rx/empty)))
|
(rx/empty)))
|
||||||
|
|
||||||
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
;; `:line-height` is stored on both the paragraph and its spans, and
|
||||||
(if-not (empty? attrs)
|
;; the renderer takes the larger of the two.
|
||||||
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
(let [pattrs (if wasm-editing-selection?
|
||||||
(rx/empty)))
|
(conj txt/paragraph-attrs :line-height)
|
||||||
|
txt/paragraph-attrs)
|
||||||
|
attrs (select-keys attrs pattrs)
|
||||||
|
result (when (and (seq attrs) wasm-editing?)
|
||||||
|
(wasm.api/apply-paragraph-attrs-to-selection attrs))]
|
||||||
|
(cond
|
||||||
|
(empty? attrs)
|
||||||
|
(rx/empty)
|
||||||
|
|
||||||
|
(some? result)
|
||||||
|
(rx/of (v2-update-text-shape-content
|
||||||
|
(:shape-id result) (:content result)
|
||||||
|
:update-name? true))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))))
|
||||||
|
|
||||||
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
||||||
(if-not (empty? attrs)
|
(if (or (empty? attrs) wasm-editing-selection?)
|
||||||
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
(rx/empty)
|
||||||
(rx/empty)))
|
(rx/of (update-text-attrs {:id id :attrs attrs}))))
|
||||||
|
|
||||||
(when (and (features/active-feature? state "text-editor/v2")
|
(when (and (features/active-feature? state "text-editor/v2")
|
||||||
(not (features/active-feature? state "text-editor-wasm/v1")))
|
(not (features/active-feature? state "text-editor-wasm/v1")))
|
||||||
|
|||||||
@ -203,7 +203,7 @@
|
|||||||
(fn [^js event]
|
(fn [^js event]
|
||||||
(when (text-editor/text-editor-has-focus?)
|
(when (text-editor/text-editor-has-focus?)
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(when (text-editor/text-editor-get-selection)
|
(when (text-editor/text-editor-has-selection?)
|
||||||
(let [text (text-editor/text-editor-export-selection)]
|
(let [text (text-editor/text-editor-export-selection)]
|
||||||
(.setData (.-clipboardData event) "text/plain" text))))))
|
(.setData (.-clipboardData event) "text/plain" text))))))
|
||||||
|
|
||||||
@ -212,7 +212,7 @@
|
|||||||
(fn [^js event]
|
(fn [^js event]
|
||||||
(when (text-editor/text-editor-has-focus?)
|
(when (text-editor/text-editor-has-focus?)
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(when (text-editor/text-editor-get-selection)
|
(when (text-editor/text-editor-has-selection?)
|
||||||
(let [text (text-editor/text-editor-export-selection)]
|
(let [text (text-editor/text-editor-export-selection)]
|
||||||
(.setData (.-clipboardData event) "text/plain" (or text ""))
|
(.setData (.-clipboardData event) "text/plain" (or text ""))
|
||||||
(when (and text (seq text))
|
(when (and text (seq text))
|
||||||
@ -268,6 +268,15 @@
|
|||||||
(sync-wasm-text-editor-content!)
|
(sync-wasm-text-editor-content!)
|
||||||
(wasm.api/request-render-preserving-target "text-delete-forward"))
|
(wasm.api/request-render-preserving-target "text-delete-forward"))
|
||||||
|
|
||||||
|
;; Shift+Tab falls through to the browser, so the keyboard can
|
||||||
|
;; still leave the editor.
|
||||||
|
(and (= key "Tab") (not shift?))
|
||||||
|
(do
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(text-editor/text-editor-insert-text "\t")
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render-preserving-target "text-tab"))
|
||||||
|
|
||||||
;; Insert
|
;; Insert
|
||||||
(= key "Insert")
|
(= key "Insert")
|
||||||
(do
|
(do
|
||||||
|
|||||||
@ -711,6 +711,14 @@
|
|||||||
(request-render "apply-styles-to-selection")
|
(request-render "apply-styles-to-selection")
|
||||||
result))
|
result))
|
||||||
|
|
||||||
|
(defn apply-paragraph-attrs-to-selection
|
||||||
|
"Apply paragraph attrs to the paragraphs the editor selection touches.
|
||||||
|
Returns {:shape-id :content} for saving."
|
||||||
|
[attrs]
|
||||||
|
(let [result (text-editor/apply-paragraph-attrs-to-selection attrs use-shape set-shape-text-content)]
|
||||||
|
(request-render "apply-paragraph-attrs-to-selection")
|
||||||
|
result))
|
||||||
|
|
||||||
(defn set-parent-id
|
(defn set-parent-id
|
||||||
[id]
|
[id]
|
||||||
(let [buffer (uuid/get-u32 id)]
|
(let [buffer (uuid/get-u32 id)]
|
||||||
|
|||||||
@ -762,3 +762,29 @@
|
|||||||
:content new-content}
|
:content new-content}
|
||||||
with-fills?
|
with-fills?
|
||||||
(assoc :fills (selection-fills new-content normalized-selection)))))))))))
|
(assoc :fills (selection-fills new-content normalized-selection)))))))))))
|
||||||
|
|
||||||
|
(defn apply-paragraph-attrs-to-selection
|
||||||
|
"Apply paragraph level attrs (text-align, text-direction) to the whole
|
||||||
|
paragraphs the editor selection touches; a collapsed caret means just the one
|
||||||
|
it sits in."
|
||||||
|
[attrs use-shape-fn set-shape-text-content-fn]
|
||||||
|
(when (wasm/ready?)
|
||||||
|
(let [shape-id (text-editor-get-active-shape-id)
|
||||||
|
selection (text-editor-get-selection)]
|
||||||
|
(when (and shape-id selection)
|
||||||
|
(when-let [content (get-cached-content shape-id)]
|
||||||
|
(let [{:keys [start-para end-para]} (normalize-selection selection)
|
||||||
|
paragraph-set (first (:children content))
|
||||||
|
new-paragraphs (into []
|
||||||
|
(map-indexed (fn [idx para]
|
||||||
|
(if (<= start-para idx end-para)
|
||||||
|
(merge para attrs)
|
||||||
|
para)))
|
||||||
|
(:children paragraph-set))
|
||||||
|
new-content (assoc content :children
|
||||||
|
[(assoc paragraph-set :children new-paragraphs)])]
|
||||||
|
(update-cached-content! shape-id new-content)
|
||||||
|
(use-shape-fn shape-id)
|
||||||
|
(set-shape-text-content-fn shape-id new-content)
|
||||||
|
{:shape-id shape-id
|
||||||
|
:content new-content}))))))
|
||||||
|
|||||||
@ -3,8 +3,8 @@ use crate::{
|
|||||||
error::Result,
|
error::Result,
|
||||||
math::Rect,
|
math::Rect,
|
||||||
shapes::{
|
shapes::{
|
||||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, ParagraphLayout, Stroke,
|
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
||||||
StrokeKind, TextContent,
|
ParagraphLayout, Stroke, StrokeKind, TextContent,
|
||||||
},
|
},
|
||||||
utils::{get_fallback_fonts, get_font_collection},
|
utils::{get_fallback_fonts, get_font_collection},
|
||||||
};
|
};
|
||||||
@ -55,7 +55,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
paragraph.line_height(),
|
paragraph.line_height(),
|
||||||
);
|
);
|
||||||
builder.push_style(&stroke_style);
|
builder.push_style(&stroke_style);
|
||||||
builder.add_text(&text);
|
add_text_with_tabs(builder, &text, span.font_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,10 @@ use skia_safe::{
|
|||||||
textlayout::Affinity,
|
textlayout::Affinity,
|
||||||
textlayout::ParagraphBuilder,
|
textlayout::ParagraphBuilder,
|
||||||
textlayout::ParagraphStyle,
|
textlayout::ParagraphStyle,
|
||||||
|
textlayout::PlaceholderAlignment,
|
||||||
|
textlayout::PlaceholderStyle,
|
||||||
textlayout::PositionWithAffinity,
|
textlayout::PositionWithAffinity,
|
||||||
|
textlayout::TextBaseline,
|
||||||
Contains,
|
Contains,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -725,7 +728,7 @@ impl TextContent {
|
|||||||
has_text = true;
|
has_text = true;
|
||||||
}
|
}
|
||||||
builder.push_style(&text_style);
|
builder.push_style(&text_style);
|
||||||
builder.add_text(&text);
|
add_text_with_tabs(&mut builder, &text, span.font_size);
|
||||||
}
|
}
|
||||||
if !has_text {
|
if !has_text {
|
||||||
builder.add_text(" ");
|
builder.add_text(" ");
|
||||||
@ -759,7 +762,7 @@ impl TextContent {
|
|||||||
has_text = true;
|
has_text = true;
|
||||||
}
|
}
|
||||||
builder.push_style(&text_style);
|
builder.push_style(&text_style);
|
||||||
builder.add_text(&text);
|
add_text_with_tabs(&mut builder, &text, span.font_size);
|
||||||
}
|
}
|
||||||
if !has_text {
|
if !has_text {
|
||||||
builder.add_text(" ");
|
builder.add_text(" ");
|
||||||
@ -1213,7 +1216,7 @@ impl Paragraph {
|
|||||||
style.set_height(self.line_height);
|
style.set_height(self.line_height);
|
||||||
style.set_text_align(self.text_align);
|
style.set_text_align(self.text_align);
|
||||||
style.set_text_direction(self.text_direction);
|
style.set_text_direction(self.text_direction);
|
||||||
style.set_replace_tab_characters(true);
|
style.set_replace_tab_characters(false);
|
||||||
style.set_apply_rounding_hack(true);
|
style.set_apply_rounding_hack(true);
|
||||||
style.set_text_height_behavior(skia::textlayout::TextHeightBehavior::All);
|
style.set_text_height_behavior(skia::textlayout::TextHeightBehavior::All);
|
||||||
style
|
style
|
||||||
@ -1249,12 +1252,30 @@ fn capitalize_words(text: &str) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter control characters below U+0020, preserving line breaks.
|
/// Add `text`, pushing every '\t' as a one em wide placeholder.
|
||||||
|
pub fn add_text_with_tabs(builder: &mut ParagraphBuilder, text: &str, font_size: f32) {
|
||||||
|
let tab = PlaceholderStyle::new(
|
||||||
|
font_size,
|
||||||
|
0.0,
|
||||||
|
PlaceholderAlignment::Baseline,
|
||||||
|
TextBaseline::Alphabetic,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (index, segment) in text.split('\t').enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
builder.add_placeholder(&tab);
|
||||||
|
}
|
||||||
|
builder.add_text(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter control characters below U+0020, preserving tabs and line breaks.
|
||||||
/// Browser-dependent: Firefox drops them, others replace with space.
|
/// Browser-dependent: Firefox drops them, others replace with space.
|
||||||
fn process_ignored_chars(text: &str, browser: u8) -> String {
|
fn process_ignored_chars(text: &str, browser: u8) -> String {
|
||||||
text.chars()
|
text.chars()
|
||||||
.filter_map(|c| {
|
.filter_map(|c| {
|
||||||
if c == '\n' || c == '\r' || c == '\u{2028}' || c == '\u{2029}' {
|
if c == '\t' || c == '\n' || c == '\r' || c == '\u{2028}' || c == '\u{2029}' {
|
||||||
return Some(c);
|
return Some(c);
|
||||||
}
|
}
|
||||||
if c < '\u{0020}' {
|
if c < '\u{0020}' {
|
||||||
@ -1742,6 +1763,15 @@ mod tests {
|
|||||||
assert_eq!(process_ignored_chars("hello\rworld", 0), "hello\rworld");
|
assert_eq!(process_ignored_chars("hello\rworld", 0), "hello\rworld");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_ignored_chars_preserves_tabs() {
|
||||||
|
assert_eq!(process_ignored_chars("hello\tworld", 0), "hello\tworld");
|
||||||
|
assert_eq!(
|
||||||
|
process_ignored_chars("hello\tworld", Browser::Firefox as u8),
|
||||||
|
"hello\tworld"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_ignored_chars_replaces_control_chars_chrome() {
|
fn process_ignored_chars_replaces_control_chars_chrome() {
|
||||||
// U+0001 (SOH) should become space in non-Firefox
|
// U+0001 (SOH) should become space in non-Firefox
|
||||||
|
|||||||
@ -1090,7 +1090,7 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 {
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> bool {
|
pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> bool {
|
||||||
with_state!(state, {
|
with_state!(state, {
|
||||||
if !get_text_editor_state().selection.is_selection() {
|
if get_text_editor_state().active_shape_id.is_none() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user