mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 21:56:50 -06:00
Merge PR #1406: Object: Corner Options opens a dialog laid out as the reference app's, also from the Properties Corner link
Conflicts: crates/ui-egui/src/i18n/*.rs (table, ar, cs, de, it, ja, ka, pl, pt_br, ru, uk): kept both sides' keys (#1414's Stroke Type/Corner Shape, #1406's Corner Size and Shape:), no duplicates
This commit is contained in:
@@ -464,6 +464,7 @@ Newest first.
|
||||
- **2026-10-21** · Properties › Bullets and Numbering › Options opens the Bullets and Numbering dialog for the selected paragraphs, as InDesign's, instead of the Paragraph panel. Found in the Learn Skills Daily course, chapter 21. No readiness percentage change.
|
||||
- **2026-10-20** · Properties › Appearance: the corner shape list shows each shape's glyph beside its name, drawn as InDesign draws them (Fancy has its own glyph now), and the stroke type list shows each type's line; both dropdowns are named for screen readers. No readiness percentage change.
|
||||
- **2026-10-21** · Properties › Align: the Align To button shows what the objects align to, one glyph each for the selection, the key object, the margins, the page and the spread, as InDesign's. Found in the Learn Skills Daily course, chapter 12. No readiness percentage change.
|
||||
- **2026-10-20** · Object › Corner Options opens a Corner Options dialog laid out as InDesign's: each corner's size and shape, the link button that makes all four the same, and Preview. The Corner link in Properties › Appearance opens it too (it opened a Rectangle size dialog). No readiness percentage change.
|
||||
- **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
|
||||
- **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
|
||||
- **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
|
||||
|
||||
@@ -607,7 +607,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Corner Options…",
|
||||
["Object"],
|
||||
None,
|
||||
"{shape: none|rounded|inverseRounded|inset|bevel|fancy, size, corners?: [{shape, size}] (4 entries in path order: top-left, top-right, bottom-right, bottom-left; replaces shape and size), ids?}",
|
||||
"{shape: none|rounded|inverseRounded|inset|bevel|fancy, size, corners?: [{shape, size} | null] (4 entries in path order: top-left, top-right, bottom-right, bottom-left; replaces shape and size; null keeps that corner of each object), ids?}",
|
||||
has_selection,
|
||||
corner_options
|
||||
),
|
||||
@@ -2161,19 +2161,31 @@ fn corner_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use designcraft_geom::corners::{Corner, CornerOptions, CornerShape};
|
||||
let opts = match p.get("corners") {
|
||||
Some(v) => {
|
||||
let corners: [Corner; 4] =
|
||||
serde_json::from_value(v.clone()).map_err(|e| bad("object.cornerOptions", format!("corners: four {{shape, size}} entries ({e})")))?;
|
||||
if corners.iter().any(|c| !c.size.is_finite() || c.size < 0.0) {
|
||||
let corners: [Option<Corner>; 4] = serde_json::from_value(v.clone())
|
||||
.map_err(|e| bad("object.cornerOptions", format!("corners: four {{shape, size}} or null entries ({e})")))?;
|
||||
if corners.iter().flatten().any(|c| !c.size.is_finite() || c.size < 0.0) {
|
||||
return Err(bad("object.cornerOptions", "corner sizes must be finite and not negative"));
|
||||
}
|
||||
CornerOptions { corners }
|
||||
corners
|
||||
}
|
||||
None => {
|
||||
let shape = p.get("shape").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or(CornerShape::Rounded);
|
||||
CornerOptions::uniform(shape, f64_or(p, "size", 12.0))
|
||||
CornerOptions::uniform(shape, f64_or(p, "size", 12.0)).corners.map(Some)
|
||||
}
|
||||
};
|
||||
set_flag(s, p, move |i| i.corners = opts, false)
|
||||
// A null corner keeps each object's own.
|
||||
set_flag(
|
||||
s,
|
||||
p,
|
||||
move |i| {
|
||||
for (c, new) in i.corners.corners.iter_mut().zip(opts) {
|
||||
if let Some(new) = new {
|
||||
*c = new;
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// `object.matchAttributes`: the selected objects (or `ids`) take `from`'s appearance.
|
||||
|
||||
@@ -2950,6 +2950,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
STEP_AND_REPEAT => step_and_repeat_dialog(app, ui, &mut d),
|
||||
CORNER_OPTIONS => corner_options_dialog(app, ui, &mut d),
|
||||
TOC => toc_dialog(app, ui, &mut d),
|
||||
id if id.starts_with("cmd:") => command_form(app, ui, &mut d),
|
||||
"goToPage" => {
|
||||
@@ -3903,6 +3904,22 @@ fn confirm_part6(app: &mut DesignApp, d: Dialog) -> Result<Value, String> {
|
||||
app.session.reset_find_scope();
|
||||
result
|
||||
}
|
||||
CORNER_OPTIONS => {
|
||||
// The preview goes; the corners change once, as one undo step.
|
||||
if d.fields.contains_key(PREVIEWED) {
|
||||
app.session.cancel_interaction();
|
||||
}
|
||||
let mut d = d;
|
||||
seed_corner_options(app, &mut d);
|
||||
link_corners(&mut d);
|
||||
let r = corner_options_params(&d).and_then(|p| app.run("object.cornerOptions", p));
|
||||
if let Err(e) = &r {
|
||||
d.fields.remove(PREVIEWED);
|
||||
d.fields.insert("status".into(), json!(e));
|
||||
app.ui.dialog = Some(d);
|
||||
}
|
||||
r
|
||||
}
|
||||
STEP_AND_REPEAT => {
|
||||
// The preview goes; the copies are made once, as one undo step.
|
||||
if d.fields.contains_key(PREVIEWED) {
|
||||
@@ -6749,6 +6766,7 @@ fn command_form(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
|
||||
const STEP_AND_REPEAT: &str = "cmd:edit.stepAndRepeat";
|
||||
pub(crate) const CORNER_OPTIONS: &str = "cmd:object.cornerOptions";
|
||||
const TOC: &str = "cmd:toc.generate";
|
||||
|
||||
/// One included style of the Table of Contents dialog, as `toc.generate` takes it, with the
|
||||
@@ -7108,6 +7126,206 @@ fn step_and_repeat_preview(app: &mut DesignApp, d: &mut Dialog) {
|
||||
d.fields.insert(PREVIEWED.into(), params);
|
||||
}
|
||||
|
||||
/// The corners of Object › Corner Options, in `object.cornerOptions` path order (top-left,
|
||||
/// top-right, bottom-right, bottom-left), with the tip InDesign gives each.
|
||||
const CORNERS: [(&str, &str); 4] = [("0", "Top Left:"), ("1", "Top Right:"), ("2", "Bottom Right:"), ("3", "Bottom Left:")];
|
||||
|
||||
/// The Corner Options dialog's fields from the first selected object's corners, once: each
|
||||
/// corner's size (in the ruler units) and shape, linked when all four are the same.
|
||||
fn seed_corner_options(app: &DesignApp, d: &mut Dialog) {
|
||||
use designcraft_geom::corners::Corner;
|
||||
let Some(st) = app.session.active() else { return };
|
||||
d.unit = st.doc.settings.horizontal_units;
|
||||
if d.fields.contains_key("_seeded") {
|
||||
return;
|
||||
}
|
||||
let corners: [Corner; 4] = st.selection.items.iter().find_map(|id| st.doc.item(*id)).map(|it| it.corners.corners).unwrap_or_default();
|
||||
for ((k, _), c) in CORNERS.iter().zip(corners) {
|
||||
d.fields.entry(format!("size{k}")).or_insert(json!(format_measure(c.size, d.unit)));
|
||||
d.fields.entry(format!("shape{k}")).or_insert(serde_json::to_value(c.shape).unwrap_or(json!("none")));
|
||||
}
|
||||
let first = corners[0];
|
||||
d.fields.entry("link").or_insert(json!(corners.iter().all(|c| *c == first)));
|
||||
d.fields.insert("_seed".into(), json!(corner_fields(d)));
|
||||
d.fields.insert("_seeded".into(), json!(true));
|
||||
}
|
||||
|
||||
/// Each corner's size and shape fields, in `CORNERS` order.
|
||||
fn corner_fields(d: &Dialog) -> Vec<(String, String)> {
|
||||
CORNERS.iter().map(|(k, _)| (d.s(&format!("size{k}")), d.s(&format!("shape{k}")))).collect()
|
||||
}
|
||||
|
||||
/// The corners edited since the dialog opened: changed from the first object's, or set by a
|
||||
/// linked edit (all four, even one that was already the value).
|
||||
fn edited_corners(d: &Dialog) -> [bool; 4] {
|
||||
let touched: [bool; 4] = d.fields.get("_touched").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
let seed: Vec<(String, String)> = d.fields.get("_seed").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
let now = corner_fields(d);
|
||||
std::array::from_fn(|i| touched[i] || seed.get(i) != now.get(i))
|
||||
}
|
||||
|
||||
/// Linked corners: a corner changed since the dialog last looked (size or shape, typed or set
|
||||
/// over the control channel) is all four.
|
||||
fn link_corners(d: &mut Dialog) {
|
||||
let now = corner_fields(d);
|
||||
let seen: Vec<(String, String)> = d.fields.get("_corners").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_else(|| now.clone());
|
||||
let mut now = now;
|
||||
let mut touched: [bool; 4] = d.fields.get("_touched").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
for (t, (a, b)) in touched.iter_mut().zip(now.iter().zip(&seen)) {
|
||||
*t |= a != b;
|
||||
}
|
||||
if d.b("link")
|
||||
&& let Some(i) = now.iter().zip(&seen).position(|(a, b)| a != b)
|
||||
&& let Some(changed) = now.get(i).cloned()
|
||||
{
|
||||
for (k, _) in &CORNERS {
|
||||
d.fields.insert(format!("size{k}"), json!(changed.0));
|
||||
d.fields.insert(format!("shape{k}"), json!(changed.1));
|
||||
}
|
||||
now = vec![changed; CORNERS.len()];
|
||||
touched = [true; 4];
|
||||
}
|
||||
d.fields.insert("_corners".into(), json!(now));
|
||||
d.fields.insert("_touched".into(), json!(touched));
|
||||
}
|
||||
|
||||
/// `object.cornerOptions`' params from the dialog: the corners edited, the others null (each
|
||||
/// selected object keeps its own). A size that does not parse, or a shape the dialog doesn't
|
||||
/// offer, is an error to show.
|
||||
fn corner_options_params(d: &Dialog) -> Result<Value, String> {
|
||||
use designcraft_geom::corners::CornerShape;
|
||||
let edited = edited_corners(d);
|
||||
let mut corners = Vec::with_capacity(CORNERS.len());
|
||||
for ((k, _), edited) in CORNERS.iter().zip(edited) {
|
||||
let key = format!("size{k}");
|
||||
let size =
|
||||
d.m(&key).filter(|v| v.is_finite() && *v >= 0.0).ok_or_else(|| format!("Enter a valid corner size, e.g. 12 or 3 mm: {}", d.s(&key)))?;
|
||||
let shape: CornerShape = match d.fields.get(&format!("shape{k}")) {
|
||||
None => CornerShape::default(),
|
||||
Some(v) => serde_json::from_value(v.clone()).map_err(|_| format!("Choose a corner shape: {v} isn't one"))?,
|
||||
};
|
||||
corners.push(if edited { json!({"shape": shape, "size": size}) } else { Value::Null });
|
||||
}
|
||||
Ok(json!({"corners": corners}))
|
||||
}
|
||||
|
||||
/// A corner's icon: the corner of a small square, with a dot at the corner it sets.
|
||||
fn paint_corner_icon(ui: &mut egui::Ui, corner: usize, tip: &str) {
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
let (r, resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||
resp.on_hover_text(tip);
|
||||
let b = r.shrink(3.0);
|
||||
ui.painter().rect_stroke(b, 0.0, egui::Stroke::new(1.0, t.icon), egui::StrokeKind::Inside);
|
||||
let at = match corner {
|
||||
0 => b.left_top(),
|
||||
1 => b.right_top(),
|
||||
2 => b.right_bottom(),
|
||||
_ => b.left_bottom(),
|
||||
};
|
||||
ui.painter().rect_filled(egui::Rect::from_center_size(at, egui::vec2(4.0, 4.0)), 0.0, t.accent);
|
||||
}
|
||||
|
||||
/// Object › Corner Options, as InDesign's: under "Corner Size and Shape:", each corner's size and
|
||||
/// shape (top corners above, bottom ones below, as on the frame), the link button that makes all
|
||||
/// four the same, and Preview.
|
||||
fn corner_options_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
use designcraft_geom::corners::CornerShape;
|
||||
seed_corner_options(app, d);
|
||||
link_corners(d);
|
||||
let lang = app.ui.language.clone();
|
||||
let tr = |s: &str| crate::i18n::tr(&lang, s).to_string();
|
||||
crate::rtl::label(ui, tr("Corner Size and Shape:"));
|
||||
ui.add_space(8.0);
|
||||
let corner = |ui: &mut egui::Ui, d: &mut Dialog, i: usize| {
|
||||
let Some((k, tip)) = CORNERS.get(i) else { return };
|
||||
ui.horizontal(|ui| {
|
||||
paint_corner_icon(ui, i, &tr(tip));
|
||||
measure_field(ui, d, &format!("size{k}"), 70.0);
|
||||
let key = format!("shape{k}");
|
||||
let shape: CornerShape = d.fields.get(&key).and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
let resp = crate::panels::properties::corner_shape_button(ui, shape, 46.0);
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::ComboBox, true, format!("{} {}", tr(tip), tr(shape.label()))));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(150.0);
|
||||
for s in CornerShape::ALL {
|
||||
if ui.selectable_label(s == shape, crate::rtl::widget(ui, tr(s.label()))).clicked() {
|
||||
d.fields.insert(key.clone(), json!(s));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
corner(ui, d, 0);
|
||||
ui.add_space(6.0);
|
||||
corner(ui, d, 3);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
let linked = d.b("link");
|
||||
let tip = tr("Make all settings the same");
|
||||
let resp = ui
|
||||
.vertical(|ui| {
|
||||
ui.add_space(12.0);
|
||||
crate::icons::button(ui, if linked { "link" } else { "link-broken" }, 20.0, linked, &tip)
|
||||
})
|
||||
.inner;
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &tip));
|
||||
if resp.clicked() {
|
||||
d.fields.insert("link".into(), json!(!linked));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
corner(ui, d, 1);
|
||||
ui.add_space(6.0);
|
||||
corner(ui, d, 2);
|
||||
});
|
||||
});
|
||||
link_corners(d);
|
||||
ui.add_space(10.0);
|
||||
check(ui, d, "preview", &tr("Preview"));
|
||||
corner_options_preview(app, d);
|
||||
if let Some(st) = d.fields.get("status").and_then(Value::as_str) {
|
||||
ui.add_space(6.0);
|
||||
crate::rtl::label(ui, egui::RichText::new(st).color(crate::theme::Tokens::get(ui.ctx()).text_dim));
|
||||
}
|
||||
}
|
||||
|
||||
/// Preview on: the corners show on the frame (redone when a field changes); off: they go back.
|
||||
fn corner_options_preview(app: &mut DesignApp, d: &mut Dialog) {
|
||||
let shown = d.fields.get(PREVIEWED).cloned();
|
||||
if !d.b("preview") {
|
||||
if shown.is_some() {
|
||||
app.session.cancel_interaction();
|
||||
d.fields.remove(PREVIEWED);
|
||||
d.fields.remove("status");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let params = match corner_options_params(d) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
d.fields.insert("status".into(), json!(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if shown.as_ref() == Some(¶ms) {
|
||||
return;
|
||||
}
|
||||
if shown.is_none()
|
||||
&& let Err(e) = app.session.begin_interaction("Corner Options")
|
||||
{
|
||||
d.fields.insert("status".into(), json!(e.to_string()));
|
||||
return;
|
||||
}
|
||||
match app.session.preview("object.cornerOptions", params.clone()) {
|
||||
Ok(()) => d.fields.remove("status"),
|
||||
Err(e) => d.fields.insert("status".into(), json!(e.to_string())),
|
||||
};
|
||||
d.fields.insert(PREVIEWED.into(), params);
|
||||
}
|
||||
|
||||
/// New Cross-Reference: link to a paragraph (paragraph styles on the left, their paragraphs on
|
||||
/// the right) or a text anchor, with a cross-reference format.
|
||||
fn insert_xref(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
|
||||
@@ -3521,3 +3521,116 @@ fn character_style_options_set_the_digits() {
|
||||
assert_eq!(ctl(&mut app, "ui.dialog.confirm", json!({}))["ok"], true);
|
||||
assert_eq!(char_style(&app, "Emphasis").unwrap().chars.digits, Some(designcraft_doc::Digits::Farsi));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_options_dialog_sets_each_corner_with_preview_and_one_undo_step() {
|
||||
use designcraft_geom::corners::CornerShape;
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let id = app.run("frame.create", json!({"rect": [72, 72, 360, 360]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
let corners = |app: &DesignApp| app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().corners.corners;
|
||||
let undo = |app: &DesignApp| app.session.doc().unwrap().history.undo.len();
|
||||
let set = |app: &mut DesignApp, k: &str, v: Value| {
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), v);
|
||||
};
|
||||
let u0 = undo(&app);
|
||||
let ctx = dialog_context("en");
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 800.0));
|
||||
// Object › Corner Options… opens InDesign's dialog, not a form of the command's params.
|
||||
crate::menus::activate(&mut app, "object.cornerOptions", &Value::Null);
|
||||
assert_eq!(app.ui.dialog.as_ref().map(|d| d.id.as_str()), Some(CORNER_OPTIONS));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
let labels = dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
for expected in ["Corner Options", "Corner Size and Shape:", "Preview", "OK", "Cancel"] {
|
||||
visible_label(&labels, screen, expected);
|
||||
}
|
||||
assert!(!labels.iter().any(|(t, _, _)| t.trim() == "Ids"), "no raw command fields");
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.s("size1"), d.s("shape1"), d.b("link")), ("0p0".into(), "none".into(), true), "seeded from the frame, linked");
|
||||
// Linked: a corner changed is all four.
|
||||
set(&mut app, "size0", json!("1p0"));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().s("size2"), "1p0");
|
||||
// Unlinked, only the top-right corner rounds, as in the tutorial.
|
||||
set(&mut app, "link", json!(false));
|
||||
for k in 0..4 {
|
||||
set(&mut app, &format!("size{k}"), json!("0"));
|
||||
}
|
||||
set(&mut app, "size1", json!("3 cm"));
|
||||
set(&mut app, "shape1", json!("rounded"));
|
||||
set(&mut app, "preview", json!(true));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
assert_eq!(corners(&app)[1].shape, CornerShape::Rounded, "Preview shows it");
|
||||
assert_eq!(undo(&app), u0, "without an undo step");
|
||||
set(&mut app, "preview", json!(false));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
assert_eq!(corners(&app)[1].shape, CornerShape::None, "Preview off puts it back");
|
||||
set(&mut app, "preview", json!(true));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
confirm(&mut app).unwrap();
|
||||
let c = corners(&app);
|
||||
assert_eq!(c.map(|c| c.shape), [CornerShape::None, CornerShape::Rounded, CornerShape::None, CornerShape::None]);
|
||||
assert!((c[1].size - 3.0 / 2.54 * 72.0).abs() < 1e-6, "{}", c[1].size);
|
||||
assert_eq!(undo(&app), u0 + 1, "one undo step");
|
||||
// Reopened, it shows the frame's corners, no longer linked.
|
||||
crate::menus::activate(&mut app, "object.cornerOptions", &Value::Null);
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.s("shape1"), d.s("shape0"), d.b("link")), ("rounded".into(), "none".into(), false));
|
||||
// A size that does not parse keeps the dialog open with the error; so does a shape the
|
||||
// dialog doesn't offer (rather than another shape taking its place).
|
||||
set(&mut app, "size0", json!("abc"));
|
||||
assert!(confirm(&mut app).is_err());
|
||||
assert!(app.ui.dialog.is_some());
|
||||
set(&mut app, "size0", json!("0"));
|
||||
set(&mut app, "shape0", json!("spiky"));
|
||||
assert!(confirm(&mut app).is_err());
|
||||
assert!(app.ui.dialog.is_some());
|
||||
cancel(&mut app);
|
||||
assert_eq!(corners(&app), c);
|
||||
}
|
||||
|
||||
/// Several objects: the dialog shows the first one's corners, and OK sets only the corners
|
||||
/// edited; the others keep each object's own. A linked edit sets all four.
|
||||
#[test]
|
||||
fn corner_options_keep_each_objects_untouched_corners() {
|
||||
use designcraft_geom::corners::CornerShape;
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 200, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("frame.create", json!({"rect": [272, 72, 400, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("object.cornerOptions", json!({"ids": [b], "shape": "bevel", "size": 12})).unwrap();
|
||||
app.run("selection.set", json!({"ids": [a, b]})).unwrap();
|
||||
let corners = |app: &DesignApp, id: u64| app.session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().corners.corners;
|
||||
let set = |app: &mut DesignApp, k: &str, v: Value| {
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert(k.into(), v);
|
||||
};
|
||||
let ctx = dialog_context("en");
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1200.0, 800.0));
|
||||
let b0 = corners(&app, b);
|
||||
// Unlinked: only the top-right corner rounds, on both.
|
||||
crate::menus::activate(&mut app, "object.cornerOptions", &Value::Null);
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
set(&mut app, "link", json!(false));
|
||||
set(&mut app, "size1", json!("1p0"));
|
||||
set(&mut app, "shape1", json!("rounded"));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
confirm(&mut app).unwrap();
|
||||
let (ca, cb) = (corners(&app, a), corners(&app, b));
|
||||
assert_eq!(ca.map(|c| c.shape), [CornerShape::None, CornerShape::Rounded, CornerShape::None, CornerShape::None]);
|
||||
assert_eq!(cb.map(|c| c.shape), [CornerShape::Bevel, CornerShape::Rounded, CornerShape::Bevel, CornerShape::Bevel], "b keeps its others");
|
||||
assert_eq!((cb[0], cb[2], cb[3]), (b0[0], b0[2], b0[3]));
|
||||
assert_eq!(ca[1], cb[1]);
|
||||
// Linked: a corner set to the first object's own value still sets all four, on both.
|
||||
crate::menus::activate(&mut app, "object.cornerOptions", &Value::Null);
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
set(&mut app, "link", json!(true));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
set(&mut app, "size0", json!("0p6"));
|
||||
dialog_frame(&mut app, &ctx, screen, vec![]);
|
||||
confirm(&mut app).unwrap();
|
||||
let (ca, cb) = (corners(&app, a), corners(&app, b));
|
||||
assert_eq!(ca, cb, "all four, on both");
|
||||
assert!(ca.iter().all(|c| c.shape == CornerShape::None && (c.size - 6.0).abs() < 1e-6), "{ca:?}");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "لصق"),
|
||||
("Stroke Type", "نوع الحد"),
|
||||
("Corner Shape", "شكل الزاوية"),
|
||||
("Corner Size and Shape:", "حجم الزوايا وشكلها:"),
|
||||
("Restart Numbering", "إعادة الترقيم"),
|
||||
("Continue Numbering", "متابعة الترقيم"),
|
||||
("Keep Options…", "خيارات الإبقاء…"),
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Vložit"),
|
||||
("Stroke Type", "Typ tahu"),
|
||||
("Corner Shape", "Tvar rohu"),
|
||||
("Corner Size and Shape:", "Velikost a tvar rohů:"),
|
||||
("Restart Numbering", "Začít číslování znovu"),
|
||||
("Continue Numbering", "Pokračovat v číslování"),
|
||||
("Keep Options…", "Volby zachování…"),
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Einfügen"),
|
||||
("Stroke Type", "Konturtyp"),
|
||||
("Corner Shape", "Eckenform"),
|
||||
("Corner Size and Shape:", "Eckengröße und -form:"),
|
||||
("Restart Numbering", "Nummerierung neu beginnen"),
|
||||
("Continue Numbering", "Nummerierung fortführen"),
|
||||
("Keep Options…", "Umbruchoptionen …"),
|
||||
|
||||
@@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Incolla"),
|
||||
("Stroke Type", "Tipo traccia"),
|
||||
("Corner Shape", "Forma angolo"),
|
||||
("Corner Size and Shape:", "Dimensione e forma angoli:"),
|
||||
("Restart Numbering", "Riavvia numerazione"),
|
||||
("Continue Numbering", "Continua numerazione"),
|
||||
("Keep Options…", "Opzioni di separazione…"),
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "ペースト"),
|
||||
("Stroke Type", "線の種類"),
|
||||
("Corner Shape", "角のシェイプ"),
|
||||
("Corner Size and Shape:", "角のサイズとシェイプ:"),
|
||||
("Restart Numbering", "番号の振り直し"),
|
||||
("Continue Numbering", "番号の継続"),
|
||||
("Keep Options…", "段落分離禁止オプション…"),
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "ჩასმა"),
|
||||
("Stroke Type", "კონტურის ტიპი"),
|
||||
("Corner Shape", "კუთხის ფორმა"),
|
||||
("Corner Size and Shape:", "კუთხეების ზომა და ფორმა:"),
|
||||
("Restart Numbering", "ნუმერაციის თავიდან დაწყება"),
|
||||
("Continue Numbering", "ნუმერაციის გაგრძელება"),
|
||||
("Keep Options…", "შენარჩუნების პარამეტრები…"),
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Wklej"),
|
||||
("Stroke Type", "Rodzaj obrysu"),
|
||||
("Corner Shape", "Kształt narożnika"),
|
||||
("Corner Size and Shape:", "Rozmiar i kształt narożników:"),
|
||||
("Restart Numbering", "Zacznij numerowanie od nowa"),
|
||||
("Continue Numbering", "Kontynuuj numerowanie"),
|
||||
("Keep Options…", "Opcje utrzymywania…"),
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Colar"),
|
||||
("Stroke Type", "Tipo de traçado"),
|
||||
("Corner Shape", "Forma do canto"),
|
||||
("Corner Size and Shape:", "Tamanho e forma dos cantos:"),
|
||||
("Restart Numbering", "Reiniciar numeração"),
|
||||
("Continue Numbering", "Continuar numeração"),
|
||||
("Keep Options…", "Opções de manutenção…"),
|
||||
|
||||
@@ -36,6 +36,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Вставить"),
|
||||
("Stroke Type", "Тип обводки"),
|
||||
("Corner Shape", "Форма угла"),
|
||||
("Corner Size and Shape:", "Размер и форма углов:"),
|
||||
("Restart Numbering", "Начать нумерацию заново"),
|
||||
("Continue Numbering", "Продолжить нумерацию"),
|
||||
("Keep Options…", "Параметры удержания…"),
|
||||
|
||||
@@ -123,6 +123,10 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]),
|
||||
("Stroke Type", ["Stroke Type", "Type de contour", "Tipo de trazo", "Stroke Type", "描边类型"]),
|
||||
("Corner Shape", ["Corner Shape", "Forme d'angle", "Forma de esquina", "Corner Shape", "角形状"]),
|
||||
(
|
||||
"Corner Size and Shape:",
|
||||
["Eckengröße und -form:", "Taille et forme des angles:", "Tamaño y forma de esquinas:", "角のサイズとシェイプ:", "角大小和形状:"],
|
||||
),
|
||||
("Hide Rulers", ["Lineale ausblenden", "Masquer les règles", "Ocultar reglas", "定規を隠す", "隐藏标尺"]),
|
||||
("Inches Decimal", ["Zoll dezimal", "Pouces décimaux", "Pulgadas decimales", "インチ(10 進)", "十进制英寸"]),
|
||||
(
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Вставити"),
|
||||
("Stroke Type", "Тип обведення"),
|
||||
("Corner Shape", "Форма кута"),
|
||||
("Corner Size and Shape:", "Розмір і форма кутів:"),
|
||||
("Restart Numbering", "Почати нумерацію знову"),
|
||||
("Continue Numbering", "Продовжити нумерацію"),
|
||||
("Keep Options…", "Параметри утримання…"),
|
||||
|
||||
@@ -33,7 +33,7 @@ pub use links::{info_panel, links_panel, preflight_panel, update_links};
|
||||
#[cfg(test)]
|
||||
use object::{align_to_icon, preview_row};
|
||||
use object::{align_row, appearance_section, guide_section, quick_actions_object, transform_section, wrap_section};
|
||||
pub(crate) use object::{corner_type_dropdown, ref_point_proxy, stroke_type_dropdown};
|
||||
pub(crate) use object::{corner_shape_button, corner_type_dropdown, ref_point_proxy, stroke_type_dropdown};
|
||||
use object_panels::pathfinder_row;
|
||||
pub use object_panels::{align_panel, attributes_panel, effects_panel, pathfinder_panel, stroke_panel, wrap_panel};
|
||||
pub(crate) use text::{LANGUAGES, kerning_control, language_menu};
|
||||
|
||||
@@ -310,6 +310,11 @@ fn paint_corner_shape(p: &egui::Painter, r: Rect, shape: CornerShape, c: egui::C
|
||||
}
|
||||
}
|
||||
|
||||
/// A corner shape picker's face: the shape's glyph and the dropdown arrow (the menu is the caller's).
|
||||
pub(crate) fn corner_shape_button(ui: &mut Ui, shape: CornerShape, width: f32) -> egui::Response {
|
||||
preview_dropdown(ui, width, |p, r| paint_corner_shape(p, r, shape))
|
||||
}
|
||||
|
||||
pub(crate) fn corner_type_dropdown(app: &mut DesignApp, ui: &mut Ui, shape: CornerShape, size: f64, width: f32) {
|
||||
let resp =
|
||||
preview_dropdown(ui, width, crate::i18n::tr(&app.ui.language, "Corner Shape"), |p, r| paint_corner_shape(p, r, shape, egui::Color32::BLACK));
|
||||
@@ -388,7 +393,8 @@ pub(super) fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo)
|
||||
let b = block(ui, FIELD_H);
|
||||
icons::paint(ui.painter(), sub(b, 3.0, 1.0, 19.0, 19.0), "corner", t.accent);
|
||||
if place(ui, sub(b, 34.0, 0.0, 56.0, FIELD_H), |ui| link_label(ui, crate::i18n::tr(&app.ui.language, "Corner"), 56.0)).clicked() {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("frameSize", json!({})));
|
||||
// Corner Options, as Object › Corner Options… opens it.
|
||||
crate::menus::activate(app, "object.cornerOptions", &Value::Null);
|
||||
}
|
||||
let (shape, size) = i.corner;
|
||||
if let Some(v) = place(ui, sub(b, 96.0, 0.0, 69.0, FIELD_H), |ui| {
|
||||
|
||||
Reference in New Issue
Block a user