mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 14:16:49 -06:00
Merge PR #1521: Paragraph Border and Shading: link the four sides, as InDesign's
This commit is contained in:
@@ -455,6 +455,7 @@ Newest first.
|
||||
- **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is an undo step, is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change.
|
||||
- **2026-10-16** · IDML: a page keeps column guides that were dragged off centre (InDesign's Custom columns). They were divided evenly on import, so the gutter sprang back to the middle; the imported positions now place the columns, with the canvas guides, snapping and text placement following them, and they are written back on export. Layout › Margins and Columns re-divides them evenly when the count or gutter changes, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-16** · IDML: custom column positions are checked before they are used (#1096). A set must give two edges per column, ascending from the left margin to the right one; any other (a wrong count, unsorted or non-numeric values, a list longer than 216 columns allow, or positions left behind by a margin or page-size change) is the even split, in the canvas, on import and on export, instead of columns drawn outside the margins. No readiness percentage change.
|
||||
- **2026-10-21** · Paragraph Border and Paragraph Shading link their four sides as InDesign's: the stroke weights, offsets and corners have the "Make all settings the same" link, on when the four are the same, so a side typed in sets all four; with the link off a side is set alone (a border with only a thick left side). Found in the Learn Skills Daily course, chapter 40. 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.
|
||||
|
||||
@@ -4575,18 +4575,32 @@ fn para_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &
|
||||
/// edited side stores the whole array.
|
||||
fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, field: &dyn Fn(&mut egui::Ui, &str, Option<f64>) -> Option<f64>) {
|
||||
let mut sides: [f64; 4] = serde_json::from_value(para_value(d, pv, key)).unwrap_or_default();
|
||||
let linked = linked(d, key, sides.iter().all(|v| (v - sides[0]).abs() < 1e-9));
|
||||
let mut changed = false;
|
||||
egui::Grid::new(("pssides", key)).num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
// As InDesign's: Top and Bottom, the link, Left and Right.
|
||||
egui::Grid::new(("pssides", key)).num_columns(5).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
for (i, label) in ["Top:", "Left:", "Bottom:", "Right:"].into_iter().enumerate() {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, label));
|
||||
if let Some(side) = sides.get_mut(i)
|
||||
&& let Some(v) = field(ui, &format!("ps{key}{i}"), Some(*side))
|
||||
{
|
||||
*side = v;
|
||||
if let Some(v) = field(ui, &format!("ps{key}{i}"), sides.get(i).copied()) {
|
||||
// Linked, a side typed in sets all four.
|
||||
if linked {
|
||||
sides = [v; 4];
|
||||
} else if let Some(side) = sides.get_mut(i) {
|
||||
*side = v;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if i % 2 == 1 {
|
||||
ui.end_row();
|
||||
match i {
|
||||
// Linked again, the four take Top's value.
|
||||
0 if link_toggle(lang, ui, d, key, linked) => {
|
||||
sides = [sides[0]; 4];
|
||||
changed = true;
|
||||
}
|
||||
0 => {}
|
||||
2 => {
|
||||
ui.label("");
|
||||
}
|
||||
_ => ui.end_row(),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -4595,6 +4609,23 @@ fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &s
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the four values of `key` are linked ("Make all settings the same"): as the user left
|
||||
/// the link, else when the four are the same, as InDesign starts it.
|
||||
fn linked(d: &Dialog, key: &str, same: bool) -> bool {
|
||||
d.fields.get(&format!("_linked.{key}")).and_then(Value::as_bool).unwrap_or(same)
|
||||
}
|
||||
|
||||
/// The link button between a border's or shading's four sides or corners; true when it was
|
||||
/// clicked to link them, and the caller makes the four the same.
|
||||
fn link_toggle(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, key: &str, linked: bool) -> bool {
|
||||
let tip = crate::i18n::tr(lang, "Make all settings the same");
|
||||
let clicked = crate::widgets::icon_toggle_sized(ui, if linked { "link" } else { "link-broken" }, linked, tip, egui::vec2(22.0, 21.0)).clicked();
|
||||
if clicked {
|
||||
d.fields.insert(format!("_linked.{key}"), json!(!linked));
|
||||
}
|
||||
clicked && !linked
|
||||
}
|
||||
|
||||
/// A swatch menu with a colour chip per swatch (hidden swatches left out); returns the swatch
|
||||
/// picked.
|
||||
fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui::AsIdSalt, current: &str, width: f32) -> Option<String> {
|
||||
@@ -4649,14 +4680,16 @@ fn border_type_menu(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, d
|
||||
fn para_corners(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, units: Unit) {
|
||||
use designcraft_geom::corners::{Corner, CornerShape};
|
||||
let mut corners: [Corner; 4] = serde_json::from_value(para_value(d, pv, key)).unwrap_or_default();
|
||||
let mut changed = false;
|
||||
egui::Grid::new(("pscorners", key)).num_columns(3).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
let linked = linked(d, key, corners.iter().all(|c| *c == corners[0]));
|
||||
let mut changed = None;
|
||||
let mut relinked = false;
|
||||
egui::Grid::new(("pscorners", key)).num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
for (i, label) in ["Top Left:", "Top Right:", "Bottom Right:", "Bottom Left:"].into_iter().enumerate() {
|
||||
let Some(c) = corners.get_mut(i) else { continue };
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, label));
|
||||
if let Some(v) = crate::widgets::measure(ui, &format!("ps{key}{i}"), Some(c.size), units, 70.0) {
|
||||
c.size = if v.is_finite() { v.clamp(0.0, 10_000.0) } else { 0.0 };
|
||||
changed = true;
|
||||
changed = Some(i);
|
||||
}
|
||||
craft_ui::ComboBox::from_id_salt(("pscornershape", key, i))
|
||||
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, c.shape.label())))
|
||||
@@ -4665,14 +4698,26 @@ fn para_corners(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key:
|
||||
for shape in CornerShape::ALL {
|
||||
if ui.selectable_label(c.shape == shape, crate::rtl::widget(ui, crate::i18n::tr(lang, shape.label()))).clicked() {
|
||||
c.shape = shape;
|
||||
changed = true;
|
||||
changed = Some(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Linked again, the four take Top Left's size and shape.
|
||||
if i == 0 && link_toggle(lang, ui, d, key, linked) {
|
||||
relinked = true;
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
if changed {
|
||||
if relinked {
|
||||
corners = [corners[0]; 4];
|
||||
changed = Some(0);
|
||||
}
|
||||
if let Some(i) = changed {
|
||||
// Linked, the corner changed sets all four.
|
||||
if linked {
|
||||
corners = [corners[i]; 4];
|
||||
}
|
||||
para_set(d, key, serde_json::to_value(corners).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3521,3 +3521,79 @@ 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));
|
||||
}
|
||||
|
||||
/// Replace the text of the field beside the `nth` label `label` (on its row, to its right) with
|
||||
/// `new`, and leave the field.
|
||||
fn retype_beside(h: &mut egui_kittest::Harness<'static, crate::test_window::Window>, label: &str, nth: usize, new: &str) {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let l = h.get_all_by_label(label).nth(nth).unwrap_or_else(|| panic!("no label {label} #{nth}")).rect();
|
||||
let field = h
|
||||
.get_all_by(|n| n.role() == egui::accesskit::Role::TextInput)
|
||||
.map(|n| n.rect())
|
||||
.filter(|r| r.center().x > l.max.x && (r.center().y - l.center().y).abs() < l.height().max(8.0))
|
||||
.min_by(|a, b| a.min.x.total_cmp(&b.min.x))
|
||||
.unwrap_or_else(|| panic!("no field beside {label} #{nth}"))
|
||||
.center();
|
||||
crate::test_window::click_at(h, field);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Text(new.into()));
|
||||
h.run_steps(1);
|
||||
// Tab leaves the field (Enter would be the dialog's OK).
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// The course's chapter 40: Paragraph Border's four weights are linked as InDesign's, so a
|
||||
/// side typed in sets all four; with the link off ("Make all settings the same") a side is set
|
||||
/// alone, as the lesson gives the border only a thick left side. The corners link the same way.
|
||||
#[test]
|
||||
fn paragraph_border_sides_and_corners_link_as_indesigns() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let (app, _) = ruled_paragraph();
|
||||
let mut h = crate::test_window::open(app, egui::vec2(1400.0, 900.0));
|
||||
h.state_mut().app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"name": "Ruled Child", "section": "border", "p.borderOn": true})));
|
||||
h.run_steps(4);
|
||||
let field = |h: &egui_kittest::Harness<'static, crate::test_window::Window>, key: &str| {
|
||||
h.state().app.ui.dialog.as_ref().and_then(|d| d.fields.get(&format!("p.{key}")).cloned()).unwrap_or_default()
|
||||
};
|
||||
// The four weights start the same, so linked: Top sets them all.
|
||||
retype_beside(&mut h, "Top:", 0, "3");
|
||||
assert_eq!(field(&h, "borderWeights"), json!([3.0, 3.0, 3.0, 3.0]));
|
||||
// Unlinked, Left alone takes 10 pt.
|
||||
// The link of the weights (0), corners (1) or offsets (2), where it is now: the dialog
|
||||
// scrolls as fields take focus.
|
||||
let link = |h: &egui_kittest::Harness<'static, crate::test_window::Window>, n: usize| {
|
||||
let links: Vec<egui::Rect> = h.get_all_by_label("Make all settings the same").map(|n| n.rect()).collect();
|
||||
assert_eq!(links.len(), 3, "weights, corners and offsets each have a link");
|
||||
links[n].center()
|
||||
};
|
||||
let at = link(&h, 0);
|
||||
crate::test_window::click_at(&mut h, at);
|
||||
retype_beside(&mut h, "Left:", 0, "10");
|
||||
assert_eq!(field(&h, "borderWeights"), json!([3.0, 10.0, 3.0, 3.0]));
|
||||
// Linked again, the four take Top's weight; unlinked once more for the thick left side.
|
||||
let at = link(&h, 0);
|
||||
crate::test_window::click_at(&mut h, at);
|
||||
assert_eq!(field(&h, "borderWeights"), json!([3.0, 3.0, 3.0, 3.0]));
|
||||
let at = link(&h, 0);
|
||||
crate::test_window::click_at(&mut h, at);
|
||||
retype_beside(&mut h, "Left:", 0, "10");
|
||||
assert_eq!(field(&h, "borderWeights"), json!([3.0, 10.0, 3.0, 3.0]));
|
||||
// The corners start the same too: Top Left's size goes to all four.
|
||||
retype_beside(&mut h, "Top Left:", 0, "0p4");
|
||||
let corners = field(&h, "borderCorners");
|
||||
assert!((0..4).all(|i| corners[i]["size"] == json!(4.0)), "{corners}");
|
||||
// Unlinked, Top Right alone takes 0p8; linked again, all four take Top Left's 4 pt.
|
||||
let at = link(&h, 1);
|
||||
crate::test_window::click_at(&mut h, at);
|
||||
retype_beside(&mut h, "Top Right:", 0, "0p8");
|
||||
assert_eq!(field(&h, "borderCorners")[1]["size"], json!(8.0));
|
||||
assert_eq!(field(&h, "borderCorners")[0]["size"], json!(4.0));
|
||||
let at = link(&h, 1);
|
||||
crate::test_window::click_at(&mut h, at);
|
||||
let corners = field(&h, "borderCorners");
|
||||
assert!((0..4).all(|i| corners[i] == corners[0] && corners[i]["size"] == json!(4.0)), "{corners}");
|
||||
confirm(&mut h.state_mut().app).unwrap();
|
||||
let style = h.state().app.session.doc().unwrap().doc.styles.para("Ruled Child").cloned().unwrap();
|
||||
assert_eq!(style.para.border_weights, Some([3.0, 10.0, 3.0, 3.0]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user