Merge pull request #4713 from dotCooCoo/fix/new-style-highlight

Character and Paragraph Styles: a style made by the New style dialog or the panel menu is highlighted
This commit is contained in:
BFlat
2026-10-31 00:49:40 +07:00
committed by GitHub
4 changed files with 142 additions and 3 deletions
+7 -2
View File
@@ -48,13 +48,18 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
}
/// OK: make the style with the name. A name another style has, or an empty one, keeps the dialog
/// open.
/// open. The panel highlights the new style and scrolls it into view when it next draws.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let mut params = d.fields.get("__target").filter(|t| t.is_object()).cloned().unwrap_or_else(|| json!({}));
params["name"] = json!(d.str("name"));
let r = app.run(if para(d) { "paraStyle.new" } else { "charStyle.new" }, params);
if r.is_ok() {
if let Ok(v) = &r {
app.ui.dialog = None;
if let Some((uid, name)) = app.session.active().and_then(|doc| v["name"].as_str().map(|name| (doc.uid, name))) {
let para = para(d);
app.ui.made_text_style.retain(|(pending_uid, pending_para, _)| *pending_uid != uid || *pending_para != para);
app.ui.made_text_style.push((uid, para, name.to_string()));
}
}
r
}
+15 -1
View File
@@ -227,6 +227,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
// A highlighted style that is no longer listed (renamed in Style Options, deleted by a command
// or undone) is no longer highlighted.
let mut sel = highlighted(app, ui.ctx(), kind, &listed);
// A style made by the New style dialog waits for this kind's panel on its own document.
app.ui.made_text_style.retain(|(uid, _, _)| app.session.documents().iter().any(|doc| doc.uid == *uid));
if let Some(index) = app
.ui
.made_text_style
.iter()
.position(|(uid, para, _)| *para == (kind == Kind::Para) && app.session.active().is_some_and(|doc| doc.uid == *uid))
{
let (_, _, name) = app.ui.made_text_style.remove(index);
highlight_made(ui.ctx(), kind, vec![name]);
sel = highlighted(app, ui.ctx(), kind, &listed);
}
let scroll_to: Option<String> = pstate(ui.ctx(), kind.scroll_key());
widgets::list_box(ui, |ui| {
for (name, attrs, _) in &all {
@@ -330,7 +342,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
if menu_item(ui, &crate::i18n::fmt(tl!("New {kind} Style…"), &[("kind", kind.what_label())]), true, false) {
// A later typing preview must not restore the document from before this capture.
end_typing(app);
app.run(&format!("{p}.new"), target(app, kind, json!({}))).ok();
if let Ok(r) = app.run(&format!("{p}.new"), target(app, kind, json!({}))) {
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string).into_iter().collect());
}
}
if menu_item(ui, &crate::i18n::fmt(tl!("Duplicate {kind} Style"), &[("kind", kind.what_label())]), !many.is_empty(), false) {
let names: Vec<String> = listed.iter().filter(|n| many.iter().any(|s| s == *n)).map(|n| n.to_string()).collect();
@@ -368,6 +368,15 @@ fn capture_from_menu(range: Option<(usize, usize)>, redefine: bool) {
capture_from_ui(range, redefine, false, false);
}
fn assert_new_style_highlight(ctx: &egui::Context, redefine: bool, name: &str) {
if !redefine {
let selected: Option<String> = pstate(ctx, Kind::Char.key());
assert_eq!(selected.as_deref(), Some(name), "New highlights the style it made");
let scroll_to: Option<String> = pstate(ctx, Kind::Char.scroll_key());
assert_eq!(scroll_to.as_deref(), Some(name), "New requests a scroll to the new style");
}
}
fn capture_from_ui(range: Option<(usize, usize)>, redefine: bool, footer: bool, unicode: bool) {
let (mut app, id, ctx) = capture_app();
let (first, tail) = if unicode { ("é", "漢字") } else { ("AB", "cdef") };
@@ -413,6 +422,7 @@ fn capture_from_ui(range: Option<(usize, usize)>, redefine: bool, footer: bool,
}
let after = app.session.doc().unwrap().doc.clone();
let name = if redefine { "Body" } else { "Character Style 1" };
assert_new_style_highlight(&ctx, redefine, name);
let definition = after.char_styles.iter().find(|s| s.name == name);
let size = definition.and_then(|s| s.attrs.get("size")).and_then(Value::as_f64);
let expected_size = if range.is_some_and(|(a, b)| b > a && a >= first.len()) { 34.0 } else { 18.0 };
@@ -840,6 +850,10 @@ fn alt_clicking_new_character_style_asks_for_its_name() {
set_name(&mut app, "Untouched");
assert!(crate::dialogs::confirm(&mut app).is_err(), "names are unique");
assert!(app.ui.dialog.is_some(), "a taken name keeps the dialog open");
assert!(app.ui.made_text_style.is_empty(), "a taken name does not request a highlight");
set_name(&mut app, "");
assert!(crate::dialogs::confirm(&mut app).is_err(), "a name is required");
assert!(app.ui.made_text_style.is_empty(), "an empty name does not request a highlight");
set_name(&mut app, "Heading");
let undo = app.session.doc().unwrap().history.undo.len();
crate::dialogs::confirm(&mut app).unwrap();
@@ -852,6 +866,107 @@ fn alt_clicking_new_character_style_asks_for_its_name() {
assert_eq!(text_editing(&app), Some((id, 3, 5)), "the Type selection stays");
}
/// The dialog highlights its new style and scrolls the matching panel to its row.
#[test]
fn a_style_made_in_the_new_style_dialog_is_highlighted() {
for kind in [Kind::Char, Kind::Para] {
let (mut app, id, ctx) = capture_app();
if kind == Kind::Para {
app.run("paraStyle.new", json!({"name": "Untouched"})).unwrap();
}
for i in 1..=12 {
app.run(&format!("{}.new", kind.prefix()), json!({"name": format!("Item {i:02}")})).unwrap();
}
select_type_range(&mut app, id, 3, 5);
let panel = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
egui::ScrollArea::vertical().id_salt("styles").max_height(80.0).show(ui, |ui| show(app, ui, kind));
};
let before = frame_events(&ctx, &mut app, vec![], panel);
let top = text_rect(&before, "Untouched").top() - 30.0;
set_pstate(&ctx, kind.key(), Some("Untouched".to_string()));
alt_click_new(&ctx, &mut app, kind);
set_name(&mut app, "Heading");
crate::dialogs::confirm(&mut app).unwrap();
let other = if kind == Kind::Char { Kind::Para } else { Kind::Char };
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, other));
assert_eq!(pstate::<Option<String>>(&ctx, other.key()), None, "{}: the other panel highlights nothing", kind.what());
assert_eq!(
app.ui.made_text_style.first().map(|(_, _, name)| name.as_str()),
Some("Heading"),
"{}: the other panel leaves the new style pending",
kind.what()
);
let mut after = vec![];
for _ in 0..3 {
let time = ctx.input(|i| i.time) + 1.0;
after = frame_raw(&ctx, &mut app, egui::RawInput { time: Some(time), ..Default::default() }, panel);
}
let highlighted: Option<String> = pstate(&ctx, kind.key());
assert_eq!(highlighted.as_deref(), Some("Heading"), "{}: the new style replaces the old highlight", kind.what());
assert!(app.ui.made_text_style.is_empty(), "{}: the panel consumes its pending highlight", kind.what());
let row = text_rect(&after, "Heading");
assert!(row.top() >= top && row.bottom() <= top + 80.0 + 30.0, "{}: Heading at {row:?}, panel from {top}", kind.what());
}
}
#[test]
fn new_character_and_paragraph_style_highlights_wait_for_their_panels() {
let (mut app, id, ctx) = capture_app();
select_type_range(&mut app, id, 3, 5);
for (kind, name) in [(Kind::Char, "Heading Char"), (Kind::Para, "Heading Para")] {
open_new(&mut app, kind).unwrap();
set_name(&mut app, name);
crate::dialogs::confirm(&mut app).unwrap();
}
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, Kind::Char));
assert_eq!(pstate::<Option<String>>(&ctx, Kind::Char.key()).as_deref(), Some("Heading Char"));
assert_eq!(app.ui.made_text_style.len(), 1);
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, Kind::Para));
assert_eq!(pstate::<Option<String>>(&ctx, Kind::Para.key()).as_deref(), Some("Heading Para"));
assert!(app.ui.made_text_style.is_empty());
}
/// A style made in one document is highlighted only when that document's panel draws.
#[test]
fn a_new_style_dialog_highlight_waits_for_its_document() {
for kind in [Kind::Char, Kind::Para] {
let (mut app, id, ctx) = capture_app();
let first_uid = app.session.active().unwrap().uid;
select_type_range(&mut app, id, 3, 5);
alt_click_new(&ctx, &mut app, kind);
set_name(&mut app, "Heading");
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(app.ui.made_text_style.first().map(|(uid, _, _)| *uid), Some(first_uid));
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.run(&format!("{}.new", kind.prefix()), json!({"name": "Heading"})).unwrap();
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, kind));
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()), None, "{}: the second document highlights nothing", kind.what());
assert!(!app.ui.made_text_style.is_empty(), "{}: the first document's highlight stays pending", kind.what());
assert!(app.session.set_active(0));
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, kind));
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()).as_deref(), Some("Heading"), "{}: the first document is highlighted", kind.what());
assert!(app.ui.made_text_style.is_empty());
}
}
#[test]
fn a_closed_document_drops_its_pending_style_highlight() {
let (mut app, id, ctx) = capture_app();
select_type_range(&mut app, id, 3, 5);
alt_click_new(&ctx, &mut app, Kind::Char);
set_name(&mut app, "Heading");
crate::dialogs::confirm(&mut app).unwrap();
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.run("charStyle.new", json!({"name": "Heading"})).unwrap();
app.session.execute("file.close", &json!({"index": 0})).unwrap();
frame_events(&ctx, &mut app, vec![], |app, ui| show(app, ui, Kind::Char));
assert_eq!(pstate::<Option<String>>(&ctx, Kind::Char.key()), None);
assert!(app.ui.made_text_style.is_empty(), "the closed document's highlight is discarded");
}
/// OK makes the style from the text's attributes as they were when the dialog opened: a later
/// format change of the Type selection, or a change of the selected objects, does not reach it.
#[test]
+5
View File
@@ -520,6 +520,10 @@ pub struct UiState {
/// dialog is open.
#[serde(skip)]
pub dialog_file: Option<std::sync::Arc<crate::dialogs::import_pdf::DialogFile>>,
/// Pending new style highlights: document uid, paragraph-style flag, and name. Each document
/// and style kind has at most one pending highlight.
#[serde(skip)]
pub made_text_style: Vec<(u64, bool, String)>,
/// The documents (uids) whose Missing Fonts dialog waits until no dialog is open, the next one to
/// show first ([`crate::dialogs::settle`]).
#[serde(skip)]
@@ -879,6 +883,7 @@ impl Default for UiState {
svg_options: Value::Null,
place_link: true,
dialog_file: None,
made_text_style: vec![],
pending_fonts: vec![],
fonts_dialog: None,
dialog_search: None,