Fix #7275: Rename artboards in Export for Screens

This commit is contained in:
makesomethingshit
2026-11-14 12:39:11 -08:00
committed by dotCooCoo
parent eb6f230d1a
commit 9334dfd8d7
5 changed files with 184 additions and 30 deletions
+3
View File
@@ -156,6 +156,9 @@ pub(crate) use effect::{FILM_GRAIN_FIELDS, PAINT_DAUB_BRUSH_TYPE_LABELS, SHADES,
pub use export_as::{export_selection, open as open_export_as};
pub(crate) use export_for_screens::drop_unread_screen_settings;
pub use export_for_screens::open as open_export_for_screens;
pub(crate) fn export_artboard_name_editing(d: &Dialog) -> bool {
d.kind == export_for_screens::KIND && d.fields.contains_key(export_for_screens::RENAME_ARTBOARD)
}
pub(crate) use export_for_screens::{
KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, export_preset as screen_export_preset, formats as screen_formats,
import_presets as screen_import_presets, open_assets as open_export_for_screens_assets, preset_list as screen_preset_list,
+110 -27
View File
@@ -391,6 +391,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
ui.vertical(|ui| {
ui.set_width(LEFT_W);
if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets")], tab) {
d.fields.remove(RENAME_ARTBOARD);
d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards")));
}
ui.add_space(6.0);
@@ -520,23 +521,32 @@ fn select_options(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, list: bool) {
let mut boards = boards(app, d);
let before = boards.clone();
let mut editing: Option<(usize, String, bool)> = d.fields.remove(RENAME_ARTBOARD).and_then(|value| serde_json::from_value(value).ok());
let mut rename = None;
let names: Vec<String> = app.session.active().map(|s| s.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default();
ui.add_enabled_ui(d.str("select") != "full", |ui| {
tiles(ui, "efs-artboards", names.len(), list, |ui, i, r| {
match artboard_thumb(app, ui.ctx(), i) {
Some(tex) => {
let sz = tex.size_vec2();
let s = (r.width() / sz.x).min(r.height() / sz.y).min(1.0);
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
ui.painter().image(tex.id(), egui::Rect::from_center_size(r.center(), sz * s), uv, egui::Color32::WHITE);
tiles(
ui,
"efs-artboards",
names.len(),
list,
|ui, i, r| {
match artboard_thumb(app, ui.ctx(), i) {
Some(tex) => {
let sz = tex.size_vec2();
let s = (r.width() / sz.x).min(r.height() / sz.y).min(1.0);
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
ui.painter().image(tex.id(), egui::Rect::from_center_size(r.center(), sz * s), uv, egui::Color32::WHITE);
}
None => {
ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE);
}
}
None => {
ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE);
}
}
let name = names.get(i).cloned().unwrap_or_default();
(format!("{} {name}", i + 1), name, boards.get(i).copied().unwrap_or(false))
})
let name = names.get(i).cloned().unwrap_or_default();
(format!("{} {name}", i + 1), name, boards.get(i).copied().unwrap_or(false))
},
Some(&mut |ui, i, rect, id, name| edit_artboard_name(ui, i, rect, id, name, &mut editing, &mut rename)),
)
.into_iter()
.for_each(|i| {
if let Some(b) = boards.get_mut(i) {
@@ -544,6 +554,16 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l
}
});
});
if d.str("select") != "full"
&& let Some(edit) = editing
{
d.fields.insert(RENAME_ARTBOARD.into(), json!(edit));
}
if let Some((i, name)) = rename
&& let Err(err) = app.run("artboard.rename", json!({"indices": [i], "name": name}))
{
app.status(err);
}
if boards != before {
set_select(d, if boards.iter().all(|b| *b) { "all" } else { "range" });
d.fields.insert("range".into(), json!(range_text(&boards)));
@@ -551,6 +571,50 @@ fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, l
}
}
/// Inline editor state belongs to this dialog, not to the document or its export settings.
pub(super) const RENAME_ARTBOARD: &str = "__renameArtboard";
fn edit_artboard_name(
ui: &mut egui::Ui,
index: usize,
rect: egui::Rect,
id: egui::Id,
name: &str,
editing: &mut Option<(usize, String, bool)>,
rename: &mut Option<(usize, String)>,
) -> bool {
if let Some((i, text, focus)) = editing.as_mut().filter(|(i, _, _)| *i == index) {
let field = id.with("field");
let cancel = ui.input(|i| i.key_pressed(egui::Key::Escape));
let enter = crate::shortcuts::pressed_anew(ui.ctx(), egui::Key::Enter);
if enter {
widgets::reject_dialog_commit(ui.ctx());
}
let response = ui.put(rect, egui::TextEdit::singleline(text).id(field).font(egui::FontId::proportional(11.0)).desired_width(rect.width()));
if *focus {
widgets::focus_field(ui, field, text);
*focus = false;
} else if cancel || enter || response.lost_focus() {
if !cancel && !text.trim().is_empty() && text.trim() != name {
*rename = Some((*i, text.trim().into()));
}
response.surrender_focus();
*editing = None;
}
false
} else {
let response = ui.interact(rect, id, egui::Sense::click());
if response.double_clicked() {
*editing = Some((index, name.into(), true));
ui.ctx().request_repaint();
}
// A double-click toggles twice, preserving the checked state while opening the editor.
response.clicked()
}
}
type CaptionEdit<'a> = dyn FnMut(&mut egui::Ui, usize, egui::Rect, egui::Id, &str) -> bool + 'a;
/// `count` thumbnails in a grid (or, with `list`, a list) that scrolls: `paint` draws thumbnail `i`
/// into its rect and gives its caption, its checkbox's name and whether it is checked → the
/// thumbnails whose checkbox (or picture) was clicked.
@@ -560,6 +624,7 @@ fn tiles(
count: usize,
list: bool,
mut paint: impl FnMut(&mut egui::Ui, usize, egui::Rect) -> (String, String, bool),
mut caption_edit: Option<&mut CaptionEdit<'_>>,
) -> Vec<usize> {
let t = Tokens::get(ui.ctx());
let mut toggled = vec![];
@@ -580,11 +645,22 @@ fn tiles(
let label_at = if list { egui::pos2(pic.right() + 8.0, pic.center().y) } else { egui::pos2(pic.left(), pic.bottom() + 9.0) };
let galley =
ui.painter().layout(caption, egui::FontId::proportional(11.0), t.text, if list { cell.x - side - 12.0 } else { side });
ui.painter().galley(egui::pos2(label_at.x, label_at.y - galley.size().y / 2.0), galley, t.text);
let label_pos = egui::pos2(label_at.x, label_at.y - galley.size().y / 2.0);
let caption_clicked = if let Some(edit) = &mut caption_edit {
let rect = egui::Rect::from_min_max(egui::pos2(label_at.x, r.top().max(label_at.y - 9.0)), r.right_bottom());
let clicked = edit(ui, i, rect, resp.id.with("name"), &name);
if !ui.memory(|m| m.has_focus(resp.id.with("name").with("field"))) {
ui.painter().galley(label_pos, galley, t.text);
}
clicked
} else {
ui.painter().galley(label_pos, galley, t.text);
false
};
// The checkbox sits on the thumbnail's lower left corner, as Illustrator's.
let bx = egui::Rect::from_min_size(egui::pos2(pic.left() + 4.0, pic.bottom() - 18.0), egui::vec2(14.0, 14.0));
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(bx));
if widgets::check_box(&mut child, &name, checked, true) || resp.clicked() {
if widgets::check_box(&mut child, &name, checked, true) || resp.clicked() || caption_clicked {
toggled.push(i);
}
}
@@ -621,18 +697,25 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, list: bool) {
// Asked for until its row is in view: the dialog's first frame only measures it.
let scroll_to = d.fields.get("__scrollTo").and_then(Value::as_u64).filter(|id| shown.iter().any(|a| a.id == *id));
let mut in_view = scroll_to.is_none();
let clicked = tiles(ui, "efs-assets", shown.len(), list, |ui, i, r| {
let Some(a) = shown.get(i) else { return (String::new(), String::new(), false) };
crate::panels::asset_export::paint_thumb(ui, st, a, r);
if scroll_to == Some(a.id) {
if ui.clip_rect().contains_rect(r) {
in_view = true;
} else {
ui.scroll_to_rect(r, Some(egui::Align::Center));
let clicked = tiles(
ui,
"efs-assets",
shown.len(),
list,
|ui, i, r| {
let Some(a) = shown.get(i) else { return (String::new(), String::new(), false) };
crate::panels::asset_export::paint_thumb(ui, st, a, r);
if scroll_to == Some(a.id) {
if ui.clip_rect().contains_rect(r) {
in_view = true;
} else {
ui.scroll_to_rect(r, Some(egui::Align::Center));
}
}
}
(a.name.clone(), a.name.clone(), on.contains(&a.id))
});
(a.name.clone(), a.name.clone(), on.contains(&a.id))
},
None,
);
for i in clicked {
if let Some(a) = shown.get(i) {
if on.contains(&a.id) {
@@ -1319,3 +1319,67 @@ fn the_dialog_builds_the_preset_list_again_only_when_the_presets_change() {
let mine = list.as_array().unwrap().iter().find(|p| p["name"] == "Mine").unwrap();
assert_eq!(mine["settings"]["formats"][0]["format"], "pdf", "{list}");
}
/// #7275: a double-click on an artboard name enters rename without leaving Export for Screens.
#[test]
fn double_clicking_an_artboard_name_in_export_for_screens_renames_it() {
for view in ["grid", "list"] {
let (mut app, log) = app(2, true);
open(&mut app);
set(&mut app, "view", json!(view));
let mut h = ScreensFrame::new();
let before = field(&app, "boards");
let out = h.settle(&mut app);
let old = app.session.active().unwrap().doc.artboards[0].name.clone();
let at = screen_text_at(&out, &format!("1 {old}"));
h.frame(&mut app, vec![egui::Event::PointerMoved(at)]);
for pressed in [true, false, true, false] {
h.frame(
&mut app,
vec![egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }],
);
}
h.settle(&mut app);
assert!(h.ctx.egui_wants_keyboard_input(), "double-clicking the artboard name must open an editable field");
h.frame(&mut app, vec![egui::Event::Text("Renamed".into())]);
h.frame(
&mut app,
vec![egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }],
);
h.settle(&mut app);
assert_eq!(app.session.active().unwrap().doc.artboards[0].name, "Renamed");
assert_eq!(kind(&app), Some(export_for_screens::KIND));
assert_eq!(field(&app, "boards"), before, "renaming must not change the checked artboards");
assert!(log.borrow().is_empty(), "renaming must not export");
// The real shortcut handler runs before the dialog body. Escape cancels only the name.
let out = h.settle(&mut app);
let at = screen_text_at(&out, "1 Renamed");
h.frame(&mut app, vec![egui::Event::PointerMoved(at)]);
for pressed in [true, false, true, false] {
h.frame(
&mut app,
vec![egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }],
);
}
h.settle(&mut app);
h.frame(&mut app, vec![egui::Event::Text("Discard".into())]);
let key = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
h.time += 0.1;
let input = egui::RawInput {
events: vec![key],
time: Some(h.time),
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))),
..Default::default()
};
let mut out = h.ctx.run_ui(input, |ui| {
crate::shortcuts::handle(&mut app, ui.ctx());
super::show(&mut app, ui.ctx());
});
out.textures_delta.clear();
h.settle(&mut app);
assert_eq!(kind(&app), Some(export_for_screens::KIND));
assert_eq!(app.session.active().unwrap().doc.artboards[0].name, "Renamed");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc.artboards[0].name, old);
}
}
+4 -1
View File
@@ -226,7 +226,10 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
// Escape closes a dropdown list or popover open in the dialog (each closes itself on it) and
// leaves the dialog open.
let popup = app.ui.dialog.as_ref().is_some_and(|d| {
egui::Popup::is_any_open(ctx) || crate::dialogs::brush_options::tips_open(d) || crate::dialogs::new_document_popup_open(d)
egui::Popup::is_any_open(ctx)
|| crate::dialogs::brush_options::tips_open(d)
|| crate::dialogs::new_document_popup_open(d)
|| crate::dialogs::export_artboard_name_editing(d)
});
// Escape in Preferences' search field with text clears the field first.
if !popup && pressed_anew(ctx, Key::Escape) && !crate::prefs_dialog::clear_search_on_escape(app, ctx) {