mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Place: arrow keys cycle the loaded images between drags (place.cycle)
This commit is contained in:
1 parent
91f78b2db9
commit
f024edb544
4 files changed
+59
-7
No files matched your search
@@ -432,6 +432,24 @@ pub(super) fn place_skip(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
Ok(json!({"remaining": loaded_count(s)}))
|
||||
}
|
||||
|
||||
/// Rotate the loaded images: `dir` 1 makes the next one current (the current one goes to the back),
|
||||
/// -1 the previous one (the last goes to the front).
|
||||
pub(super) fn place_cycle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "place.cycle";
|
||||
let dir = p.get("dir").and_then(Value::as_i64).filter(|d| matches!(d, 1 | -1)).ok_or_else(|| bad(ID, "dir must be 1 or -1"))?;
|
||||
validate_place_cursor(s, ID)?;
|
||||
if let Some(current) = s.loaded {
|
||||
if dir == 1 {
|
||||
s.loaded_queue.push_back(current);
|
||||
s.loaded = s.loaded_queue.pop_front();
|
||||
} else if let Some(last) = s.loaded_queue.pop_back() {
|
||||
s.loaded_queue.push_front(current);
|
||||
s.loaded = Some(last);
|
||||
}
|
||||
}
|
||||
Ok(json!({"remaining": loaded_count(s)}))
|
||||
}
|
||||
|
||||
/// Validate bounded grid dimensions and a finite nonnegative gutter.
|
||||
fn grid_dimensions(p: &Value, command: &str, default_columns: usize, default_rows: usize) -> Result<(usize, usize, f64)> {
|
||||
let dimension = |key, default| -> Result<usize> {
|
||||
@@ -684,6 +702,21 @@ pub(super) mod image_place_tests {
|
||||
assert!(s.execute("place.skip", &json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_rotates_the_loaded_images_both_ways() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let files: Vec<_> = (0..3).map(|i| json!({"base64": test_png(i as u8), "name": format!("{i}.png")})).collect();
|
||||
s.execute("place.load", &json!({"files": files})).unwrap();
|
||||
s.execute("place.cycle", &json!({"dir": 1})).unwrap();
|
||||
assert_eq!(loaded_names(&s), ["1.png", "2.png", "0.png"]);
|
||||
s.execute("place.cycle", &json!({"dir": -1})).unwrap();
|
||||
s.execute("place.cycle", &json!({"dir": -1})).unwrap();
|
||||
assert_eq!(loaded_names(&s), ["2.png", "0.png", "1.png"]);
|
||||
assert!(s.execute("place.cycle", &json!({"dir": 2})).is_err());
|
||||
assert!(s.execute("place.cycle", &json!({})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_path_placements_create_distinct_graphics() {
|
||||
let images = [image_file_with_color(30).unwrap(), image_file_with_color(31).unwrap(), image_file_with_color(32).unwrap()];
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::EngineError;
|
||||
use super::bytes::{base64_encode, to_bytes, to_bytes_with_preview, with_document_extension};
|
||||
use super::new::{file_new, file_sample, save_preset};
|
||||
use super::open::{file_open, file_open_bytes, file_open_recent, has_recent, recent_entry};
|
||||
use super::place::{file_place, place_contact_sheet, place_drop, place_grid, place_load, place_skip, read_source};
|
||||
use super::place::{file_place, place_contact_sheet, place_cycle, place_drop, place_grid, place_load, place_skip, read_source};
|
||||
use super::save::{FolderAccess, can_revert, file_revert, file_save, recovery_dir};
|
||||
use super::snippets::{snippet_export, snippet_place};
|
||||
use super::{CommandSpec, PRESETS, Value, always, bad, cmd, has_doc, json, ok, str_param};
|
||||
@@ -161,6 +161,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
place_contact_sheet
|
||||
),
|
||||
cmd!(noundo "place.skip", "Skip Loaded Image", [], None, "{} — drops the current loaded image without placing it and loads the next; the last one ends placing", has_doc, place_skip),
|
||||
cmd!(noundo "place.cycle", "Cycle Loaded Images", [], None, "{dir: 1|-1} — makes the next (1) or previous (-1) loaded image the current one", has_doc, place_cycle),
|
||||
cmd!(noundo "place.cancel", "Cancel Image Placement", [], None, "{} — clears every loaded image", has_doc, |s, _| {
|
||||
s.loaded = None;
|
||||
s.loaded_queue.clear();
|
||||
|
||||
+11
-2
@@ -171,7 +171,7 @@ impl Tool for PlaceGun {
|
||||
fn busy(&self) -> bool {
|
||||
self.start.is_some()
|
||||
}
|
||||
fn key(&mut self, _cx: &ToolContext, key: crate::ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
fn key(&mut self, cx: &ToolContext, key: crate::ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
use crate::ToolKey;
|
||||
// Escape drops the current image and loads the next; `place.cancel` clears them all.
|
||||
if key == ToolKey::Escape {
|
||||
@@ -190,7 +190,16 @@ impl Tool for PlaceGun {
|
||||
}
|
||||
return vec![Action::Handled];
|
||||
}
|
||||
vec![]
|
||||
// Between drags the arrows cycle the loaded images, and don't nudge the selection.
|
||||
let dir = match key {
|
||||
ToolKey::Right | ToolKey::Down => 1,
|
||||
ToolKey::Left | ToolKey::Up => -1,
|
||||
_ => return vec![],
|
||||
};
|
||||
if cx.loaded.is_none() {
|
||||
return vec![];
|
||||
}
|
||||
vec![Action::Exec("place.cycle".into(), json!({"dir": dir}))]
|
||||
}
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<crate::Overlay> {
|
||||
let Some(a) = self.start else { return vec![] };
|
||||
|
||||
@@ -2527,14 +2527,23 @@ fn place_gun_grid_arrow_keys_are_handled_during_a_drag() {
|
||||
assert!(t.key(&cx, ToolKey::Delete, Mods::default()).is_empty());
|
||||
}
|
||||
|
||||
/// Escape drops only the current loaded image; `place.cancel` clears them all.
|
||||
/// Escape drops only the current loaded image; between drags the arrows cycle the loaded images,
|
||||
/// and during a drag they keep their grid meaning.
|
||||
#[test]
|
||||
fn place_gun_escape_skips_the_current_image() {
|
||||
fn place_gun_escape_skips_and_arrows_cycle_between_drags() {
|
||||
let d = Document::new(&NewDocument::default());
|
||||
let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false));
|
||||
let cx = ctx(&d, &s, &c, &l);
|
||||
let mut cx = ctx(&d, &s, &c, &l);
|
||||
cx.loaded = Some((40.0, 20.0));
|
||||
let mut t = create("placeGun");
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::Exec("place.skip".into(), serde_json::json!({}))]);
|
||||
let exec = |cmd: &str, p: serde_json::Value| vec![Action::Exec(cmd.into(), p)];
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), exec("place.skip", serde_json::json!({})));
|
||||
for (key, dir) in [(ToolKey::Right, 1), (ToolKey::Down, 1), (ToolKey::Left, -1), (ToolKey::Up, -1)] {
|
||||
assert_eq!(t.key(&cx, key, Mods::default()), exec("place.cycle", serde_json::json!({"dir": dir})), "{key:?}");
|
||||
}
|
||||
let _ = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
|
||||
let _ = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 110.0, 60.0));
|
||||
assert_eq!(t.key(&cx, ToolKey::Right, Mods::default()), vec![Action::Handled]);
|
||||
}
|
||||
|
||||
/// A place-cursor grid drag too thin for its cells shows and places the single frame an ordinary
|
||||
|
||||
Reference in new issue
Block a user