mirror of
https://github.com/penpot/penpot.git
synced 2026-08-04 20:09:05 +00:00
🔧 Improve text editor selection and tab conversion
This commit is contained in:
parent
6e843faba3
commit
f66d588943
@ -975,7 +975,14 @@
|
||||
(watch [_ state stream]
|
||||
(let [text-editor-instance (:workspace-editor 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")
|
||||
(some? text-editor-instance))
|
||||
(rx/empty)
|
||||
@ -985,15 +992,30 @@
|
||||
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
|
||||
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
;; `:line-height` is stored on both the paragraph and its spans, and
|
||||
;; the renderer takes the larger of the two.
|
||||
(let [pattrs (if wasm-editing-selection?
|
||||
(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)]
|
||||
(if-not (empty? attrs)
|
||||
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
||||
(rx/empty)))
|
||||
(if (or (empty? attrs) wasm-editing-selection?)
|
||||
(rx/empty)
|
||||
(rx/of (update-text-attrs {:id id :attrs attrs}))))
|
||||
|
||||
(when (and (features/active-feature? state "text-editor/v2")
|
||||
(not (features/active-feature? state "text-editor-wasm/v1")))
|
||||
|
||||
@ -191,7 +191,7 @@
|
||||
(fn [^js event]
|
||||
(when (text-editor/text-editor-has-focus?)
|
||||
(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)]
|
||||
(.setData (.-clipboardData event) "text/plain" text))))))
|
||||
|
||||
@ -200,7 +200,7 @@
|
||||
(fn [^js event]
|
||||
(when (text-editor/text-editor-has-focus?)
|
||||
(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)]
|
||||
(.setData (.-clipboardData event) "text/plain" (or text ""))
|
||||
(when (and text (seq text))
|
||||
@ -256,6 +256,15 @@
|
||||
(sync-wasm-text-editor-content!)
|
||||
(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
|
||||
(= key "Insert")
|
||||
(do
|
||||
|
||||
@ -710,6 +710,14 @@
|
||||
(request-render "apply-styles-to-selection")
|
||||
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
|
||||
[id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
|
||||
@ -756,3 +756,29 @@
|
||||
:content new-content}
|
||||
with-fills?
|
||||
(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,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, ParagraphLayout, Stroke,
|
||||
StrokeKind, TextContent,
|
||||
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
||||
ParagraphLayout, Stroke, StrokeKind, TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@ -55,7 +55,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
paragraph.line_height(),
|
||||
);
|
||||
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::ParagraphBuilder,
|
||||
textlayout::ParagraphStyle,
|
||||
textlayout::PlaceholderAlignment,
|
||||
textlayout::PlaceholderStyle,
|
||||
textlayout::PositionWithAffinity,
|
||||
textlayout::TextBaseline,
|
||||
Contains,
|
||||
};
|
||||
|
||||
@ -725,7 +728,7 @@ impl TextContent {
|
||||
has_text = true;
|
||||
}
|
||||
builder.push_style(&text_style);
|
||||
builder.add_text(&text);
|
||||
add_text_with_tabs(&mut builder, &text, span.font_size);
|
||||
}
|
||||
if !has_text {
|
||||
builder.add_text(" ");
|
||||
@ -759,7 +762,7 @@ impl TextContent {
|
||||
has_text = true;
|
||||
}
|
||||
builder.push_style(&text_style);
|
||||
builder.add_text(&text);
|
||||
add_text_with_tabs(&mut builder, &text, span.font_size);
|
||||
}
|
||||
if !has_text {
|
||||
builder.add_text(" ");
|
||||
@ -1213,7 +1216,7 @@ impl Paragraph {
|
||||
style.set_height(self.line_height);
|
||||
style.set_text_align(self.text_align);
|
||||
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_text_height_behavior(skia::textlayout::TextHeightBehavior::All);
|
||||
style
|
||||
@ -1249,12 +1252,30 @@ fn capitalize_words(text: &str) -> String {
|
||||
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.
|
||||
fn process_ignored_chars(text: &str, browser: u8) -> String {
|
||||
text.chars()
|
||||
.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);
|
||||
}
|
||||
if c < '\u{0020}' {
|
||||
@ -1742,6 +1763,15 @@ mod tests {
|
||||
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]
|
||||
fn process_ignored_chars_replaces_control_chars_chrome() {
|
||||
// U+0001 (SOH) should become space in non-Firefox
|
||||
|
||||
@ -1061,7 +1061,7 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> bool {
|
||||
with_state!(state, {
|
||||
if !get_text_editor_state().selection.is_selection() {
|
||||
if get_text_editor_state().active_shape_id.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user