mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Character and Paragraph Styles: a style made by the New style dialog or the panel menu is highlighted
Squashed from #4713 onto main.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user