🐛 Sync Design tab styles on text-editor click caret (#12205)

Click caret placement only moved the selection and never refreshed
current styles, so the Design tab kept showing the previous fill and
typography. Place the caret through place_caret_from_position so
StylesChanged is emitted like keyboard navigation.

Closes #12195
This commit is contained in:
Alejandro Alonso 2026-10-08 15:42:21 +02:00 committed by GitHub
parent 2ffdd16038
commit 78d26243c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 135 additions and 4 deletions

View File

@ -566,6 +566,16 @@ impl TextEditorState {
self.push_event(TextEditorEvent::SelectionChanged);
}
/// Places the caret and refreshes `current_styles` (emits `StylesChanged`).
pub fn place_caret_from_position(
&mut self,
text_content: &TextContent,
position: &TextPositionWithAffinity,
) {
self.set_caret_from_position(position);
self.update_styles(text_content);
}
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
self.selection.extend_to(*position);
self.reset_blink();
@ -940,3 +950,125 @@ impl Default for TextEditorState {
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::Rect;
use crate::shapes::{
Fill, FontFamily, FontStyle, GrowType, Paragraph, SolidColor, TextAlign, TextSpan,
};
use skia_safe::Color;
fn span(text: &str, font_size: f32, fill: Fill) -> TextSpan {
TextSpan::new(
text.to_string(),
FontFamily::new(Uuid::nil(), 400, FontStyle::Normal),
font_size,
1.2,
0.0,
None,
None,
TextDirection::LTR,
400,
Uuid::nil(),
vec![fill],
)
}
fn hello_world_content() -> TextContent {
let black = Fill::Solid(SolidColor(Color::BLACK));
let red = Fill::Solid(SolidColor(Color::RED));
let mut content =
TextContent::new(Rect::from_xywh(0.0, 0.0, 200.0, 100.0), GrowType::Fixed);
content.add_paragraph(Paragraph::new(
TextAlign::Left,
TextDirection::LTR,
None,
None,
1.2,
0.0,
vec![span("Hello ", 24.0, black), span("world", 40.0, red)],
));
content
}
fn drain_events(editor: &mut TextEditorState) -> Vec<TextEditorEvent> {
let mut events = Vec::new();
loop {
let ev = editor.poll_event();
if ev == TextEditorEvent::None {
break;
}
events.push(ev);
}
events
}
#[test]
fn set_caret_from_position_alone_does_not_emit_styles_changed() {
let content = hello_world_content();
let mut editor = TextEditorState::new();
editor.place_caret_from_position(
&content,
&TextPositionWithAffinity::new_downstream_affinity(0, 1),
);
drain_events(&mut editor);
editor.set_caret_from_position(&TextPositionWithAffinity::new_downstream_affinity(0, 8));
let events = drain_events(&mut editor);
assert!(!events.contains(&TextEditorEvent::StylesChanged));
assert_eq!(editor.current_styles.font_size.value(), &Some(24.0));
}
#[test]
fn place_caret_from_position_syncs_styles_and_emits_styles_changed() {
let content = hello_world_content();
let mut editor = TextEditorState::new();
editor.place_caret_from_position(
&content,
&TextPositionWithAffinity::new_downstream_affinity(0, 1),
);
drain_events(&mut editor);
assert_eq!(editor.current_styles.font_size.value(), &Some(24.0));
// Offset 8 is inside the second span ("world").
editor.place_caret_from_position(
&content,
&TextPositionWithAffinity::new_downstream_affinity(0, 8),
);
let events = drain_events(&mut editor);
assert!(events.contains(&TextEditorEvent::StylesChanged));
assert_eq!(editor.current_styles.font_size.value(), &Some(40.0));
assert_eq!(
editor.current_styles.fills,
vec![Fill::Solid(SolidColor(Color::RED))]
);
}
#[test]
fn place_caret_from_position_clears_mixed_selection_styles() {
let content = hello_world_content();
let mut editor = TextEditorState::new();
editor.select_all(&content);
assert!(editor.current_styles.font_size.is_multiple());
assert!(editor.current_styles.fills_are_multiple);
drain_events(&mut editor);
editor.place_caret_from_position(
&content,
&TextPositionWithAffinity::new_downstream_affinity(0, 8),
);
let events = drain_events(&mut editor);
assert!(events.contains(&TextEditorEvent::StylesChanged));
assert_eq!(editor.current_styles.font_size.value(), &Some(40.0));
assert!(!editor.current_styles.fills_are_multiple);
assert_eq!(
editor.current_styles.fills,
vec![Fill::Solid(SolidColor(Color::RED))]
);
}
}

View File

@ -208,8 +208,7 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
let point = Point::new(x, y);
get_text_editor_state().start_pointer_selection();
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
get_text_editor_state().set_caret_from_position(&position);
get_text_editor_state().update_styles(text_content);
get_text_editor_state().place_caret_from_position(text_content, &position);
}
});
}
@ -332,7 +331,7 @@ pub extern "C" fn text_editor_set_cursor_from_offset(x: f32, y: f32) {
};
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
get_text_editor_state().set_caret_from_position(&position);
get_text_editor_state().place_caret_from_position(text_content, &position);
}
});
}
@ -359,7 +358,7 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
if let Some(position) =
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
{
get_text_editor_state().set_caret_from_position(&position);
get_text_editor_state().place_caret_from_position(text_content, &position);
}
});
}