mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Merge PR #867
This commit is contained in:
commit
c8a8a1868d
9 files changed
+161
-45
No files matched your search
@@ -136,6 +136,8 @@ Newest first.
|
||||
- **2026-10-14** · The Swatches panel has InDesign's header: a Fill/Stroke proxy, Formatting Affects Container/Text and the Tint of the colour in front (a tint alone keeps each object's own swatch). Clicking a swatch applies it to the fill or stroke in front (of the text, when formatting affects text), and the bottom row has New Color Group, New Swatch and Delete Swatch. No readiness percentage change.
|
||||
- **2026-10-14** · With the Page tool, the Control panel shows the page as InDesign's does: W/H from a reference point, page-size preset (US Business Card added), orientation, Liquid Page Rule (now with Controlled by Parent) and Show Parent Page Overlay, which draws the parent page's size over a resized page. No readiness percentage change.
|
||||
- **2026-10-14** · On an object-based page the Page tool shows a selected object's liquid pins and resize locks as InDesign does: a click pins a side to the page edge or locks the width or height, with InDesign's tooltips. No readiness percentage change.
|
||||
- **2026-10-14** · The Liquid Layout panel follows InDesign's: Liquid Page Rule with the page number, Content › Auto-Fit, and Object Constraints › Resize with Page and Pin for the selected object on an object-based page; Create Alternate Layout moved to the Layout menu. No readiness percentage change.
|
||||
- **2026-10-14** · With the Page tool, the Control panel shows the page as InDesign's does: W/H from a reference point, page-size preset (US Business Card added), orientation, Liquid Page Rule (now with Controlled by Parent) and Show Parent Page Overlay, which draws the parent page's size over a resized page. No readiness percentage change.
|
||||
- **2026-10-14** · With the Page tool, the Control panel shows the page as InDesign's does: W/H from a reference point, page-size preset (US Business Card added), orientation, Liquid Page Rule and Show Parent Page Overlay, which draws the parent page's size over a resized page. No readiness percentage change.
|
||||
- **2026-10-14** · The Page tool works as InDesign's: a click selects a page and shows its handles and liquid page rule; dragging a handle previews the rule and springs back, Alt-drag resizes the page from the opposite edge, Shift keeps proportions, with a W/H readout. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
|
||||
@@ -2516,6 +2516,14 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Added", ["Added", "Added", "Added", "Added", "已添加"]),
|
||||
("Deleted", ["Deleted", "Deleted", "Deleted", "Deleted", "已删除"]),
|
||||
("Guide-based", ["Guide-based", "Guide-based", "Guide-based", "Guide-based", "基于参考线"]),
|
||||
("Page:", ["Seite:", "Page :", "Página:", "ページ:", "页面:"]),
|
||||
("Content:", ["Inhalt:", "Contenu :", "Contenido:", "コンテンツ:", "内容:"]),
|
||||
("Object Constraints:", ["Objektbeschränkungen:", "Contraintes d’objet :", "Restricciones de objeto:", "オブジェクトの制約:", "对象约束:"]),
|
||||
(
|
||||
"Resize with Page:",
|
||||
["Mit Seite skalieren:", "Redimensionner avec la page :", "Cambiar tamaño con la página:", "ページと共にサイズ変更:", "随页面调整大小:"],
|
||||
),
|
||||
("Pin:", ["Fixieren:", "Épingler :", "Fijar:", "固定:", "固定:"]),
|
||||
("Object-Based", ["Object-Based", "Object-Based", "Object-Based", "Object-Based", "基于对象"]),
|
||||
("Guide-Based", ["Guide-Based", "Guide-Based", "Guide-Based", "Guide-Based", "基于参考线"]),
|
||||
("Resize Page", ["Seitengröße ändern", "Redimensionner la page", "Cambiar tamaño de página", "ページのサイズを変更", "调整页面大小"]),
|
||||
|
||||
@@ -718,6 +718,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "مضاف"),
|
||||
("Deleted", "محذوف"),
|
||||
("Guide-based", "استنادًا إلى الأدلة"),
|
||||
("Page:", "الصفحة:"),
|
||||
("Content:", "المحتوى:"),
|
||||
("Object Constraints:", "قيود الكائن:"),
|
||||
("Resize with Page:", "تغيير الحجم مع الصفحة:"),
|
||||
("Pin:", "تثبيت:"),
|
||||
("Object-Based", "استنادًا إلى الكائنات"),
|
||||
("Guide-Based", "استنادًا إلى الأدلة"),
|
||||
("Resize Page", "تغيير حجم الصفحة"),
|
||||
|
||||
@@ -1296,6 +1296,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Aggiunto"),
|
||||
("Deleted", "Eliminato"),
|
||||
("Guide-based", "Basato su guide"),
|
||||
("Page:", "Pagina:"),
|
||||
("Content:", "Contenuto:"),
|
||||
("Object Constraints:", "Vincoli oggetto:"),
|
||||
("Resize with Page:", "Ridimensiona con la pagina:"),
|
||||
("Pin:", "Fissa:"),
|
||||
("Object-Based", "Basato su oggetti"),
|
||||
("Guide-Based", "Basato su guide"),
|
||||
("Resize Page", "Ridimensiona pagina"),
|
||||
|
||||
@@ -1292,6 +1292,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "追加済み"),
|
||||
("Deleted", "削除済み"),
|
||||
("Guide-based", "ガイド基準"),
|
||||
("Page:", "ページ:"),
|
||||
("Content:", "コンテンツ:"),
|
||||
("Object Constraints:", "オブジェクトの制約:"),
|
||||
("Resize with Page:", "ページと共にサイズ変更:"),
|
||||
("Pin:", "固定:"),
|
||||
("Object-Based", "オブジェクト基準"),
|
||||
("Guide-Based", "ガイド基準"),
|
||||
("Resize Page", "ページのサイズを変更"),
|
||||
|
||||
@@ -709,6 +709,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Adicionado"),
|
||||
("Deleted", "Excluído"),
|
||||
("Guide-based", "Baseado em guias"),
|
||||
("Page:", "Página:"),
|
||||
("Content:", "Conteúdo:"),
|
||||
("Object Constraints:", "Restrições de objeto:"),
|
||||
("Resize with Page:", "Redimensionar com a página:"),
|
||||
("Pin:", "Fixar:"),
|
||||
("Object-Based", "Baseado em objetos"),
|
||||
("Guide-Based", "Baseado em guias"),
|
||||
("Resize Page", "Redimensionar página"),
|
||||
|
||||
@@ -1293,6 +1293,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Added", "Додано"),
|
||||
("Deleted", "Видалено"),
|
||||
("Guide-based", "На основі напрямних"),
|
||||
("Page:", "Сторінка:"),
|
||||
("Content:", "Вміст:"),
|
||||
("Object Constraints:", "Обмеження об’єкта:"),
|
||||
("Resize with Page:", "Змінювати розмір зі сторінкою:"),
|
||||
("Pin:", "Закріпити:"),
|
||||
("Object-Based", "На основі об’єктів"),
|
||||
("Guide-Based", "На основі напрямних"),
|
||||
("Resize Page", "Змінити розмір сторінки"),
|
||||
|
||||
@@ -320,6 +320,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"<",
|
||||
"cmd:layout.marginsAndColumns",
|
||||
"cmd:layout.createGuides",
|
||||
"cmd:layout.createAlternate",
|
||||
"-",
|
||||
"ui:view.goToPage",
|
||||
"-",
|
||||
|
||||
@@ -500,50 +500,104 @@ pub fn liquid_rule_label(rule: designcraft_doc::LiquidRule) -> &'static str {
|
||||
LIQUID_RULES.iter().find(|(r, _)| *r == rule).map_or("Off", |(_, l)| l)
|
||||
}
|
||||
|
||||
/// Liquid Layout panel: the current page's rule, the selection's pins (object-based), the page's
|
||||
/// liquid guides, and Create Alternate Layout.
|
||||
/// Whether page `abs` follows the object-based rule (its own, or its parent page's).
|
||||
fn object_based(d: &designcraft_doc::Document, abs: usize) -> bool {
|
||||
use designcraft_doc::LiquidRule as L;
|
||||
let Some((si, pi)) = d.page_loc(abs) else { return false };
|
||||
match d.spreads[si].pages[pi].liquid {
|
||||
L::ObjectBased => true,
|
||||
L::ControlledByParent => {
|
||||
d.parent_page_for(abs).and_then(|(ppi, ppg)| d.parents.get(ppi)?.pages.get(ppg).map(|p| p.liquid == L::ObjectBased)).unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Liquid Layout panel, laid out as InDesign's: the page's Liquid Page Rule (with "Page: n"),
|
||||
/// Content › Auto-Fit, and Object Constraints › Resize with Page (Height, Width) and Pin (Top,
|
||||
/// Left, Bottom, Right) for the selected object on an object-based page. The page's liquid
|
||||
/// guides follow (Create Alternate Layout is in the Layout menu).
|
||||
pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(page) = crate::canvas::current_page(app) else {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No document.")).size(11.0).color(t.text_dim));
|
||||
let lang = app.ui.language.clone();
|
||||
// The Page tool's page, the page in view, or (before the canvas lays out) the first.
|
||||
let Some(page) = app.session.active().map(|s| s.selection.page.or_else(|| crate::canvas::current_page(app)).unwrap_or(0)) else {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "No document.")).size(11.0).color(t.text_dim));
|
||||
return;
|
||||
};
|
||||
let Some(d) = app.session.active().map(|d| d.doc.clone()) else { return };
|
||||
let Some((si, pi)) = d.page_loc(page) else { return };
|
||||
let pg = d.spreads[si].pages[pi].clone();
|
||||
use designcraft_doc::LiquidRule as L;
|
||||
crate::rtl::label(ui, egui::RichText::new(format!("{} {}", crate::i18n::tr(&app.ui.language, "Page"), d.page_name(page))).strong());
|
||||
let mut rule = pg.liquid;
|
||||
ui.horizontal(|ui| {
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, "Liquid Page Rule:"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
crate::widgets::caption(ui, &format!("{} {}", crate::i18n::tr(&lang, "Page:"), d.page_name(page)));
|
||||
});
|
||||
});
|
||||
egui::ComboBox::from_id_salt("liquid_rule")
|
||||
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, liquid_rule_label(rule))))
|
||||
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, liquid_rule_label(pg.liquid))))
|
||||
.width(190.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (r, label) in LIQUID_RULES {
|
||||
ui.selectable_value(&mut rule, r, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label)));
|
||||
}
|
||||
});
|
||||
if rule != pg.liquid {
|
||||
let _ = app.run("liquid.pageRule", json!({"rule": rule, "pages": [page + 1]}));
|
||||
}
|
||||
if rule == L::ObjectBased
|
||||
&& let Some(id) = app.session.active().and_then(|s| s.selection.items.first().copied())
|
||||
&& let Some(it) = d.item(id)
|
||||
{
|
||||
ui.separator();
|
||||
let l = it.liquid.unwrap_or(designcraft_doc::ObjectLiquid { resize_width: true, resize_height: true, ..Default::default() });
|
||||
let mut v = [l.resize_width, l.resize_height, l.pin_top, l.pin_bottom, l.pin_left, l.pin_right];
|
||||
let keys = ["resizeWidth", "resizeHeight", "pinTop", "pinBottom", "pinLeft", "pinRight"];
|
||||
let labels = ["Resize width", "Resize height", "Pin top", "Pin bottom", "Pin left", "Pin right"];
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for ((on, key), label) in v.iter_mut().zip(keys).zip(labels) {
|
||||
if ui.checkbox(on, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).changed() {
|
||||
let _ = app.run("liquid.object", json!({key: *on}));
|
||||
if ui.selectable_label(pg.liquid == r, crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).clicked() && r != pg.liquid {
|
||||
let _ = app.run("liquid.pageRule", json!({"rule": r, "pages": [page + 1]}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
// The selected object (one on this page).
|
||||
let item = app
|
||||
.session
|
||||
.active()
|
||||
.and_then(|s| s.selection.items.first().copied())
|
||||
.and_then(|id| d.item(id))
|
||||
.filter(|it| d.page_of_item(it.id) == Some(page));
|
||||
// Content: Auto-Fit (graphic frames).
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, "Content:"));
|
||||
let graphic = item.and_then(|it| if let designcraft_doc::Content::Graphic(g) = &it.content { Some(g.auto_fit) } else { None });
|
||||
let mut auto = graphic.is_some_and(|f| f != designcraft_doc::Fitting::None);
|
||||
ui.indent("liquid_content", |ui| {
|
||||
let r = ui.add_enabled(graphic.is_some(), egui::Checkbox::new(&mut auto, crate::rtl::widget(ui, crate::i18n::tr(&lang, "Auto-Fit"))));
|
||||
if r.changed() {
|
||||
let _ = app.run("object.fittingOptions", json!({"autoFit": auto}));
|
||||
}
|
||||
});
|
||||
// Object Constraints: for an object on an object-based page.
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, "Object Constraints:"));
|
||||
let on = item.is_some() && object_based(&d, page);
|
||||
let l = item
|
||||
.filter(|_| on)
|
||||
.map(|it| it.liquid.unwrap_or(designcraft_doc::ObjectLiquid { resize_width: true, resize_height: true, ..Default::default() }))
|
||||
.unwrap_or_default();
|
||||
ui.indent("liquid_constraints", |ui| {
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
let rows: [(&str, [(&str, &str, bool); 2]); 2] = [
|
||||
("Resize with Page:", [("Height", "resizeHeight", l.resize_height), ("Width", "resizeWidth", l.resize_width)]),
|
||||
("Pin:", [("Top", "pinTop", l.pin_top), ("Left", "pinLeft", l.pin_left)]),
|
||||
];
|
||||
for (title, boxes) in rows {
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&lang, title));
|
||||
let mut grid_boxes = vec![boxes.to_vec()];
|
||||
if title == "Pin:" {
|
||||
grid_boxes.push(vec![("Bottom", "pinBottom", l.pin_bottom), ("Right", "pinRight", l.pin_right)]);
|
||||
}
|
||||
egui::Grid::new(("liquid_grid", title)).num_columns(2).min_col_width(90.0).show(ui, |ui| {
|
||||
for row in grid_boxes {
|
||||
for (label, key, value) in row {
|
||||
let mut v = value;
|
||||
if ui.checkbox(&mut v, crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).changed() {
|
||||
let _ = app.run("liquid.object", json!({key: v}));
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
if !pg.guides.is_empty() {
|
||||
ui.separator();
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Liquid guides")).size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&lang, "Liquid guides")).size(11.0).color(t.text_dim));
|
||||
for (gi, g) in pg.guides.iter().enumerate() {
|
||||
let mut on = g.liquid;
|
||||
let label = format!("{:?} at {:.1}", g.orientation, g.position);
|
||||
@@ -552,22 +606,6 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let key = egui::Id::new("alt_layout");
|
||||
let (mut name, mut w, mut h): (String, f64, f64) = ui.data(|x| x.get_temp(key)).unwrap_or(("Tablet H".into(), 1024.0, 768.0));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Create Alternate Layout")).size(11.0).color(t.text_dim));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut name).desired_width(90.0));
|
||||
ui.add(egui::DragValue::new(&mut w).range(1.0..=15552.0).suffix(" pt"));
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "×"));
|
||||
ui.add(egui::DragValue::new(&mut h).range(1.0..=15552.0).suffix(" pt"));
|
||||
});
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Create"))).clicked()
|
||||
&& let Err(e) = app.run("layout.createAlternate", json!({"name": name, "width": w, "height": h}))
|
||||
{
|
||||
app.status(format!("Alternate layout: {e}"));
|
||||
}
|
||||
ui.data_mut(|x| x.insert_temp(key, (name, w, h)));
|
||||
}
|
||||
|
||||
/// Media panel: the selected video or sound's playback options and poster.
|
||||
@@ -733,3 +771,45 @@ pub fn track_changes(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod liquid_panel_tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn object_constraints_set_the_selected_objects_pins_and_resizing() {
|
||||
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": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(260.0, 420.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let key = egui::Id::new("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(key)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(key, true));
|
||||
return;
|
||||
}
|
||||
liquid(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
let l = |h: &Harness<'_, DesignApp>| h.state().session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().liquid;
|
||||
// With the rule off the constraints don't apply.
|
||||
h.get_by_label("Right").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(l(&h), None);
|
||||
h.state_mut().run("liquid.pageRule", json!({"rule": "objectBased"})).unwrap();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Right").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Width").click();
|
||||
h.run_steps(2);
|
||||
let v = l(&h).unwrap();
|
||||
assert!(v.pin_right && !v.resize_width && v.resize_height, "{v:?}");
|
||||
let _ = h.get_by_label("Auto-Fit");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user