mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 11:16:25 +00:00
Fix #7275: Rename artboards in Export for Screens
This commit is contained in:
committed by
dotCooCoo
parent
eb6f230d1a
commit
9334dfd8d7
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user