mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-11 00:11:46 +00:00
New Guide Layout (#6083): percent of each artboard, Tab marker per pass
Percent typed in Width, Spacing or a margin was turned into pixels of the document before preview and OK, so with Selected Artboards a board smaller than the document got the document's share. It now goes to the engine as percent, which takes it of each target, and is remembered as percent. The field Tab reached stayed marked as reached while a popup kept the dialog from taking the next Tab step, so a later click into that field selected all its text. The marker now holds only for the pass that handed the focus. Co-Authored-By: safvanedge <safvanedge@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c87a538fe2
commit
08a908d3f5
3 files changed
+67
-3
No files matched your search
@@ -98,6 +98,19 @@ fn parse_length_str(text: &str, app: &PhotocraftApp, along_x: bool) -> Option<f6
|
||||
}
|
||||
}
|
||||
|
||||
/// The percent a field's text gives ("10%", or a bare number while the rulers are in percent).
|
||||
/// Percent stays percent all the way to the engine, which takes it of each target, so a selected
|
||||
/// artboard smaller than the document gets its own share, not the document's.
|
||||
fn percent_of(text: &str, ruler: Unit) -> Option<f64> {
|
||||
match Unit::parse_length(text) {
|
||||
Some((n, Some(Unit::Percent))) => Some(n),
|
||||
Some((n, None)) if ruler == Unit::Percent => Some(n),
|
||||
Some(_) => None,
|
||||
None if ruler == Unit::Percent => widgets::parse_num(text),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_stored_length(app: &PhotocraftApp, val: &Value, along_x: bool, optional: bool) -> Value {
|
||||
match val {
|
||||
Value::Number(n) => {
|
||||
@@ -108,6 +121,8 @@ fn format_stored_length(app: &PhotocraftApp, val: &Value, along_x: bool, optiona
|
||||
let t = s.trim();
|
||||
if t.is_empty() {
|
||||
if optional { Value::Null } else { json!(in_ruler_unit(app, 0.0, along_x)) }
|
||||
} else if let Some(pct) = percent_of(t, ruler_unit(app)) {
|
||||
if optional && pct <= 0.0 { Value::Null } else { json!(with_unit(&photocraft_engine::prefs::fmt_decimals(pct, FIELD_DECIMALS), Unit::Percent)) }
|
||||
} else if let Some(px) = parse_length_str(t, app, along_x) {
|
||||
if optional && px <= 0.0 { Value::Null } else { json!(in_ruler_unit(app, px, along_x)) }
|
||||
} else {
|
||||
@@ -221,6 +236,8 @@ fn parse_length_value(val: Option<&Value>, app: Option<&PhotocraftApp>, along_x:
|
||||
let t = s.trim();
|
||||
if t.is_empty() {
|
||||
if optional { Value::Null } else { json!(0.0) }
|
||||
} else if let Some(pct) = percent_of(t, app.map_or(Unit::Pixels, ruler_unit)) {
|
||||
if optional && pct <= 0.0 { Value::Null } else { json!(format!("{pct}%")) }
|
||||
} else {
|
||||
let (dpi, extent, ppi, ruler) = match app {
|
||||
Some(app) => {
|
||||
@@ -337,7 +354,8 @@ pub fn confirm(app: &mut PhotocraftApp, f: &Map<String, Value>) -> Result<Value,
|
||||
let hex = format!("#{:02x}{:02x}{:02x}", color.r(), color.g(), color.b());
|
||||
let params = params_with(Some(app), f);
|
||||
// Lengths are remembered as the pixels they applied, so a bare "5" typed in px still means
|
||||
// 5 px after the ruler unit changes; reopening shows them in the new unit.
|
||||
// 5 px after the ruler unit changes; reopening shows them in the new unit. Percent stays
|
||||
// percent, of whatever the next target is.
|
||||
let mut remembered = f.clone();
|
||||
for (key, along_x, optional) in [("width", true, true), ("height", false, true), ("gutter", true, false), ("rowGutter", false, false)] {
|
||||
if let Some(v) = f.get(key) {
|
||||
|
||||
@@ -504,3 +504,25 @@ fn tab_selects_the_field_while_a_mouse_button_is_held() {
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().ui.dialogs.first().unwrap().fields["gutter"], "5");
|
||||
}
|
||||
|
||||
/// Percent is of each selected artboard, not of the document: 50% of a 140 x 40 board is a
|
||||
/// 70 px column, centred in it; the field reopens still in percent.
|
||||
#[test]
|
||||
fn percent_is_of_the_selected_artboard_and_stays_percent() {
|
||||
let mut app = app();
|
||||
app.run("layer.new.artboard", json!({"rect": [200, 100, 140, 40]})).unwrap();
|
||||
let id = open(&mut app);
|
||||
let f = &mut app.ui.dialog_mut(id).unwrap().fields;
|
||||
f.insert("target".into(), json!("selectedArtboards"));
|
||||
f.insert("columns".into(), json!(1));
|
||||
f.insert("width".into(), json!("50 %"));
|
||||
f.insert("centerColumns".into(), json!(true));
|
||||
f.insert("clearExisting".into(), json!(true));
|
||||
f.insert("__rowsEnabled".into(), json!(false));
|
||||
f.insert("__marginEnabled".into(), json!(false));
|
||||
crate::dialogs::confirm(&mut app, id).unwrap();
|
||||
let g = &app.session.active().unwrap().doc.guides;
|
||||
assert!(g.vertical.contains(&235.0) && g.vertical.contains(&305.0), "{:?}", g.vertical);
|
||||
let reopened = open(&mut app);
|
||||
assert_eq!(app.ui.dialog_mut(reopened).unwrap().fields["width"], "50 %");
|
||||
}
|
||||
@@ -41,7 +41,10 @@ pub fn take_step(ctx: &Context) -> i32 {
|
||||
if let Some(id) = pending {
|
||||
ctx.memory_mut(|m| m.request_focus(id));
|
||||
}
|
||||
ctx.data_mut(|d| d.insert_temp(reached_id(), pending));
|
||||
// Tagged with this pass: `take_step` is skipped while a popup is open, and a marker left
|
||||
// over from an earlier pass would make a later click into the field select all its text.
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|d| d.insert_temp(reached_id(), pending.map(|id| (id, pass))));
|
||||
let step = ctx.input_mut(|i| {
|
||||
if i.consume_key(Modifiers::SHIFT, Key::Tab) {
|
||||
-1
|
||||
@@ -61,7 +64,7 @@ pub fn take_step(ctx: &Context) -> i32 {
|
||||
/// True in the frame a field gets the focus [`take_step`] handed it (Tab, ⇧Tab or [`focus`]),
|
||||
/// whatever the mouse is doing meanwhile.
|
||||
pub fn reached(ctx: &Context, id: Id) -> bool {
|
||||
ctx.data(|d| d.get_temp::<Option<Id>>(reached_id())).flatten() == Some(id)
|
||||
ctx.data(|d| d.get_temp::<Option<(Id, u64)>>(reached_id())).flatten() == Some((id, ctx.cumulative_pass_nr()))
|
||||
}
|
||||
|
||||
/// Start collecting the fields of a dialog body.
|
||||
@@ -227,6 +230,27 @@ mod tests {
|
||||
assert_eq!((d.name.as_str(), d.size.width, d.size.height), ("Flyer", 512, 310));
|
||||
}
|
||||
|
||||
/// The field a Tab reached is "reached" only in the pass that handed it focus: while a popup
|
||||
/// is open `take_step` is skipped, and a later click into the field must place the caret.
|
||||
#[test]
|
||||
fn reached_lasts_only_the_pass_that_applied_the_focus() {
|
||||
let ctx = egui::Context::default();
|
||||
let id = egui::Id::new("field");
|
||||
let mut seen = Vec::new();
|
||||
for apply in [true, false, false] {
|
||||
ctx.run_ui(Default::default(), |ui| {
|
||||
if apply {
|
||||
super::focus(ui.ctx(), id);
|
||||
super::take_step(ui.ctx());
|
||||
}
|
||||
seen.push(super::reached(ui.ctx(), id));
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
assert_eq!(seen, [true, false, false]);
|
||||
}
|
||||
|
||||
/// Tab is the dialog's: the modal underneath never sees it, and a dialog with no fields swallows it.
|
||||
#[test]
|
||||
fn tab_in_a_dialog_without_fields_does_nothing() {
|
||||
|
||||
Reference in new issue
Block a user