mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Merge PR #925
# Conflicts: # crates/ui-egui/src/i18n.rs # crates/ui-egui/src/i18n/ar.rs # crates/ui-egui/src/i18n/it.rs # crates/ui-egui/src/i18n/ja.rs # crates/ui-egui/src/i18n/pt_br.rs # crates/ui-egui/src/i18n/uk.rs
This commit is contained in:
@@ -165,6 +165,7 @@ Newest first.
|
||||
- **2026-10-14** · The Type menu follows InDesign's order (Size, Character ⌘T, Paragraph ⌥⌘T, Glyphs, the style panels, Notes, footnotes and endnotes, then the insert submenus and Show Hidden Characters); Current Page Number has ⌥⇧⌘N. No readiness percentage change.
|
||||
- **2026-10-14** · Colour parameters: a bare `{c,m,y,k}` is always read as 0..100, so C 1 is a 1 % tint and no longer
|
||||
solid cyan; fractions take the colour's own `{model: "cmyk", …}` form (#910). No readiness percentage change.
|
||||
- **2026-10-14** · The Tools panel follows InDesign's: Gradient Swatch and Gradient Feather are tools of their own; in two columns Content Collector and Placer, Apply Color / Gradient / None and Normal / Preview each get a button; View Options holds Frame Edges, Rulers, Guides, Smart Guides, Baseline Grid and Hidden Characters; Apply Color (,) and Apply Gradient (.) reuse the last colour and gradient. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
@@ -16,6 +16,9 @@ const fn t(id: &'static str, label: &'static str, shortcut: Option<&'static str>
|
||||
|
||||
/// Tools-panel groups in order (the first tool of each group shows by default). A `None` group
|
||||
/// marks a separator.
|
||||
///
|
||||
/// In two columns InDesign lays some groups out tool by tool (Content Collector and Content
|
||||
/// Placer side by side): see [`SPLIT_IN_TWO_COLUMNS`].
|
||||
pub const TOOL_GROUPS: &[&[ToolInfo]] = &[
|
||||
&[t("selection", "Selection Tool", Some("V"), "tool-selection")],
|
||||
&[t("directSelection", "Direct Selection Tool", Some("A"), "tool-direct")],
|
||||
@@ -62,10 +65,8 @@ pub const TOOL_GROUPS: &[&[ToolInfo]] = &[
|
||||
t("scale", "Scale Tool", Some("S"), "tool-scale"),
|
||||
t("shear", "Shear Tool", Some("O"), "tool-shear"),
|
||||
],
|
||||
&[
|
||||
t("gradientSwatch", "Gradient Swatch Tool", Some("G"), "tool-gradient"),
|
||||
t("gradientFeather", "Gradient Feather Tool", Some("Shift+G"), "tool-gradient-feather"),
|
||||
],
|
||||
&[t("gradientSwatch", "Gradient Swatch Tool", Some("G"), "tool-gradient")],
|
||||
&[t("gradientFeather", "Gradient Feather Tool", Some("Shift+G"), "tool-gradient-feather")],
|
||||
&[],
|
||||
&[t("note", "Note Tool", None, "tool-note")],
|
||||
&[
|
||||
@@ -77,6 +78,10 @@ pub const TOOL_GROUPS: &[&[ToolInfo]] = &[
|
||||
&[t("zoom", "Zoom Tool", Some("Z"), "tool-zoom")],
|
||||
];
|
||||
|
||||
/// Groups (by their first tool) whose tools get a button each when the Tools panel has two
|
||||
/// columns, as InDesign's does.
|
||||
pub const SPLIT_IN_TWO_COLUMNS: &[&str] = &["contentCollector"];
|
||||
|
||||
pub fn tool_info(id: &str) -> Option<ToolInfo> {
|
||||
TOOL_GROUPS.iter().flat_map(|g| g.iter()).find(|t| t.id == id).copied()
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use designcraft_geom::{Affine, BezPath, Point, Rect, Unit, Vec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
pub use catalog::{TOOL_GROUPS, ToolInfo, tool_for_shortcut, tool_info};
|
||||
pub use catalog::{SPLIT_IN_TWO_COLUMNS, TOOL_GROUPS, ToolInfo, tool_for_shortcut, tool_info};
|
||||
pub use layout::CanvasLayout;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -68,6 +68,12 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
],
|
||||
),
|
||||
("Current Page Number", ["Aktuelle Seitenzahl", "Numéro de page actuelle", "Número de página actual", "現在のページ番号", "当前页码"]),
|
||||
("Apply Color (,)", ["Farbe anwenden (,)", "Appliquer la couleur (,)", "Aplicar color (,)", "カラーを適用 (,)", "应用颜色 (,)"]),
|
||||
("Apply Gradient (.)", ["Verlauf anwenden (.)", "Appliquer le dégradé (.)", "Aplicar degradado (.)", "グラデーションを適用 (.)", "应用渐变 (.)"]),
|
||||
("View Options", ["Ansichtsoptionen", "Options d’affichage", "Opciones de visualización", "表示オプション", "视图选项"]),
|
||||
("Frame Edges", ["Rahmenkanten", "Bords du bloc", "Bordes del marco", "フレーム枠", "框架边缘"]),
|
||||
("Rulers", ["Lineale", "Règles", "Reglas", "定規", "标尺"]),
|
||||
("Hidden Characters", ["Verborgene Zeichen", "Caractères masqués", "Caracteres ocultos", "制御文字", "隐藏字符"]),
|
||||
("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]),
|
||||
("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]),
|
||||
("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]),
|
||||
|
||||
@@ -32,6 +32,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "لصق"),
|
||||
("Bulleted & Numbered Lists", "القوائم ذات التعداد النقطي والرقمي"),
|
||||
("Current Page Number", "رقم الصفحة الحالية"),
|
||||
("Apply Color (,)", "تطبيق اللون (,)"),
|
||||
("Apply Gradient (.)", "تطبيق التدرج (.)"),
|
||||
("View Options", "خيارات العرض"),
|
||||
("Frame Edges", "حواف الإطار"),
|
||||
("Hidden Characters", "الأحرف المخفية"),
|
||||
("Reverse Path", "عكس المسار"),
|
||||
("Set Anchor", "تعيين نقطة الارتساء"),
|
||||
("Clear", "مسح"),
|
||||
|
||||
@@ -30,6 +30,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Incolla"),
|
||||
("Bulleted & Numbered Lists", "Elenchi puntati e numerati"),
|
||||
("Current Page Number", "Numero di pagina corrente"),
|
||||
("Apply Color (,)", "Applica colore (,)"),
|
||||
("Apply Gradient (.)", "Applica sfumatura (.)"),
|
||||
("View Options", "Opzioni di visualizzazione"),
|
||||
("Frame Edges", "Bordi cornice"),
|
||||
("Hidden Characters", "Caratteri nascosti"),
|
||||
("Reverse Path", "Inverti tracciato"),
|
||||
("Set Anchor", "Imposta ancoraggio"),
|
||||
("Clear", "Cancella"),
|
||||
|
||||
@@ -31,6 +31,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Copy", "コピー"),
|
||||
("Paste", "ペースト"),
|
||||
("Bulleted & Numbered Lists", "箇条書き"),
|
||||
("Apply Color (,)", "カラーを適用 (,)"),
|
||||
("Apply Gradient (.)", "グラデーションを適用 (.)"),
|
||||
("View Options", "表示オプション"),
|
||||
("Frame Edges", "フレーム枠"),
|
||||
("Hidden Characters", "制御文字"),
|
||||
("Reverse Path", "パスの方向反転"),
|
||||
("Set Anchor", "アンカーポイントを設定"),
|
||||
("Clear", "消去"),
|
||||
|
||||
@@ -36,6 +36,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Current Page Number", "Número da página atual"),
|
||||
("New Note", "Nova nota"),
|
||||
("Insert Endnote", "Inserir nota de fim"),
|
||||
("Apply Color (,)", "Aplicar cor (,)"),
|
||||
("Apply Gradient (.)", "Aplicar degradê (.)"),
|
||||
("View Options", "Opções de visualização"),
|
||||
("Frame Edges", "Bordas do quadro"),
|
||||
("Hidden Characters", "Caracteres ocultos"),
|
||||
("Reverse Path", "Inverter demarcador"),
|
||||
("Set Anchor", "Definir ponto de ancoragem"),
|
||||
("Clear", "Limpar"),
|
||||
|
||||
@@ -33,6 +33,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Paste", "Вставити"),
|
||||
("Bulleted & Numbered Lists", "Марковані й нумеровані списки"),
|
||||
("Current Page Number", "Номер поточної сторінки"),
|
||||
("Apply Color (,)", "Застосувати колір (,)"),
|
||||
("Apply Gradient (.)", "Застосувати градієнт (.)"),
|
||||
("View Options", "Параметри перегляду"),
|
||||
("Frame Edges", "Краї фреймів"),
|
||||
("Hidden Characters", "Приховані символи"),
|
||||
("Reverse Path", "Розвернути контур"),
|
||||
("Set Anchor", "Задати прив’язку"),
|
||||
("Clear", "Очистити"),
|
||||
|
||||
@@ -455,6 +455,19 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32) {
|
||||
pen.line(&[(5.0, 10.0), (4.0, 10.0), (4.0, 17.0), (16.0, 17.0), (16.0, 10.0), (15.0, 10.0)]);
|
||||
}
|
||||
"screen-normal" => pen.rect(3.0, 4.0, 17.0, 16.0),
|
||||
// View Options: a frame's dashed edges with a ruler's ticks above.
|
||||
"view-options" => {
|
||||
pen.w = 1.1;
|
||||
for (x0, x1) in [(4.0, 6.5), (8.5, 11.5), (13.5, 16.0)] {
|
||||
pen.line(&[(x0, 9.0), (x1, 9.0)]);
|
||||
pen.line(&[(x0, 17.0), (x1, 17.0)]);
|
||||
}
|
||||
pen.line(&[(4.0, 9.0), (4.0, 17.0)]);
|
||||
pen.line(&[(16.0, 9.0), (16.0, 17.0)]);
|
||||
for x in [5.0, 8.0, 11.0, 14.0] {
|
||||
pen.line(&[(x, 4.0), (x, 6.0)]);
|
||||
}
|
||||
}
|
||||
"screen-preview" => {
|
||||
pen.rect(3.0, 4.0, 17.0, 16.0);
|
||||
pen.frect(6.0, 7.0, 14.0, 13.0);
|
||||
|
||||
@@ -303,6 +303,12 @@ pub struct UiState {
|
||||
pub palette: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub flyout: Option<usize>,
|
||||
/// The colour and gradient swatches applied last: what Apply Color (,) and Apply Gradient (.)
|
||||
/// apply again.
|
||||
#[serde(skip)]
|
||||
pub last_color: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub last_gradient: Option<String>,
|
||||
/// Fill/Stroke proxy (Tools and Color panels): the stroke is in front, so Apply None and the
|
||||
/// Color panel act on it (X toggles).
|
||||
#[serde(skip)]
|
||||
@@ -384,6 +390,8 @@ impl Default for UiState {
|
||||
dialog: None,
|
||||
palette: None,
|
||||
flyout: None,
|
||||
last_color: None,
|
||||
last_gradient: None,
|
||||
stroke_front: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,13 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
),
|
||||
("color.toggleFillStroke", "Toggle Fill and Stroke", Some("X"), "{} → {strokeFront} — which of fill and stroke the proxy has in front"),
|
||||
("color.applyNone", "Apply None", Some("/"), "{} — [None] for the fill or stroke in front of the proxy"),
|
||||
("color.applyColor", "Apply Color", Some(","), "{} — the colour applied last ([Black] at first) to the fill or stroke in front of the proxy"),
|
||||
(
|
||||
"color.applyGradient",
|
||||
"Apply Gradient",
|
||||
Some("."),
|
||||
"{} — the gradient applied last (else the document's first, else white to black) to the fill or stroke in front of the proxy",
|
||||
),
|
||||
("view.frameEdges", "Show/Hide Frame Edges", Some("Cmd+H"), "{}"),
|
||||
("view.rulers", "Show/Hide Rulers", Some("Cmd+R"), "{}"),
|
||||
("view.guides", "Show/Hide Guides", Some("Cmd+;"), "{}"),
|
||||
@@ -1236,6 +1243,8 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
|
||||
"color.defaultFillStroke" => crate::toolbar::default_fill_stroke(app),
|
||||
"color.swapFillStroke" => crate::toolbar::swap_fill_stroke(app),
|
||||
"color.toggleFillStroke" => crate::toolbar::toggle_fill_stroke(app),
|
||||
"color.applyColor" => crate::toolbar::apply_color(app),
|
||||
"color.applyGradient" => crate::toolbar::apply_gradient(app),
|
||||
"color.applyNone" => crate::toolbar::apply_none(app),
|
||||
"view.frameEdges" => flag(&mut app.ui.frame_edges),
|
||||
"view.overprintPreview" => {
|
||||
|
||||
@@ -75,6 +75,7 @@ pub fn color_target(app: &mut DesignApp) -> Option<(String, f32, String, f32)> {
|
||||
|
||||
/// Apply a swatch (and tint) to the fill or stroke of what colour edits apply to.
|
||||
pub fn apply_swatch(app: &mut DesignApp, stroke: bool, swatch: &str, tint: Option<f32>) -> Result<Value, String> {
|
||||
remember_swatch(app, swatch);
|
||||
if colors_affect_text(app) {
|
||||
let (k, kt) = if stroke { ("stroke", "strokeTint") } else { ("fill", "fillTint") };
|
||||
let mut attrs = json!({k: swatch});
|
||||
@@ -90,6 +91,19 @@ pub fn apply_swatch(app: &mut DesignApp, stroke: bool, swatch: &str, tint: Optio
|
||||
app.run(if stroke { "object.stroke" } else { "object.fill" }, p)
|
||||
}
|
||||
|
||||
/// Keep `swatch` as the colour or gradient Apply Color / Apply Gradient use next.
|
||||
pub fn remember_swatch(app: &mut DesignApp, swatch: &str) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
if swatch == designcraft_color::swatch::NONE {
|
||||
return;
|
||||
}
|
||||
if designcraft_color::swatch::resolve_gradient(&st.doc.swatches, swatch).is_some() {
|
||||
app.ui.last_gradient = Some(swatch.to_string());
|
||||
} else if st.doc.resolve_color(swatch, 1.0).is_some() {
|
||||
app.ui.last_color = Some(swatch.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill (or stroke) tint of the first selected item (1 = 100%).
|
||||
pub fn sel_tint(app: &DesignApp, stroke: bool) -> f32 {
|
||||
let Some(st) = app.session.active() else { return 1.0 };
|
||||
|
||||
+250
-33
@@ -1,7 +1,7 @@
|
||||
//! The Tools panel (left): single or double column, tool groups with flyouts, fill/stroke proxy,
|
||||
//! screen mode buttons.
|
||||
|
||||
use designcraft_tools::TOOL_GROUPS;
|
||||
use designcraft_tools::{SPLIT_IN_TWO_COLUMNS, TOOL_GROUPS};
|
||||
use egui::{Color32, Sense, Stroke, StrokeKind, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -45,13 +45,17 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.add_space(2.0);
|
||||
let current = app.session.tool_id();
|
||||
let mut chosen: Option<&'static str> = None;
|
||||
let mut row_items = Vec::new();
|
||||
// A slot is a group (its current tool shown, the others in its flyout) or, in two
|
||||
// columns, one tool of a group InDesign lays out tool by tool.
|
||||
let mut row_items: Vec<Option<(usize, Option<usize>)>> = Vec::new();
|
||||
for (gi, g) in TOOL_GROUPS.iter().enumerate() {
|
||||
if g.is_empty() {
|
||||
row_items.push(None);
|
||||
continue;
|
||||
} else if cols == 2 && SPLIT_IN_TWO_COLUMNS.contains(&g[0].id) {
|
||||
row_items.extend((0..g.len()).map(|k| Some((gi, Some(k)))));
|
||||
} else {
|
||||
row_items.push(Some((gi, None)));
|
||||
}
|
||||
row_items.push(Some(gi));
|
||||
}
|
||||
let mut i = 0;
|
||||
while i < row_items.len() {
|
||||
@@ -70,8 +74,12 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.add_space(8.0);
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
for _ in 0..cols {
|
||||
let Some(Some(gi)) = row_items.get(i).copied() else { break };
|
||||
let g = TOOL_GROUPS[gi];
|
||||
let Some(Some((gi, pinned))) = row_items.get(i).copied() else { break };
|
||||
let group = TOOL_GROUPS[gi];
|
||||
let g = match pinned {
|
||||
Some(k) => &group[k..=k],
|
||||
None => group,
|
||||
};
|
||||
let shown = g.iter().find(|tl| tl.id == current).unwrap_or(&g[0]);
|
||||
let active = g.iter().any(|tl| tl.id == current);
|
||||
let tip = match shown.shortcut {
|
||||
@@ -173,7 +181,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
app.ui.flyout = None;
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
fill_stroke_proxy(app, ui, width);
|
||||
fill_stroke_proxy(app, ui, width, cols);
|
||||
});
|
||||
if cols == 1 {
|
||||
let h = scroll.content_size.y;
|
||||
@@ -192,7 +200,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
/// The area under the tools (InDesign 2026 §3 group 5): mini Default Fill/Stroke and Swap, the
|
||||
/// overlapping Fill/Stroke proxy, Formatting affects container / text, the Apply None well, a
|
||||
/// utility button and the Screen Mode well.
|
||||
fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
||||
fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32, cols: usize) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let target = crate::panels::color_target(app);
|
||||
let doc = app.session.active().map(|d| d.doc.clone());
|
||||
@@ -320,42 +328,147 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
||||
}
|
||||
}
|
||||
ui.add_space(5.0);
|
||||
// Apply None well (flyout: Apply Color / Gradient / None).
|
||||
// Apply Color / Apply Gradient / Apply None: three buttons in two columns, as InDesign lays
|
||||
// them out; one well with the kind in front (and a flyout of the three) in one column.
|
||||
let front_sw = if stroke_front { &stroke_sw } else { &fill_sw };
|
||||
let front_kind = match doc.as_ref() {
|
||||
Some(_) if front_sw == designcraft_color::swatch::NONE => 2,
|
||||
Some(d) if designcraft_color::swatch::resolve_gradient(&d.swatches, front_sw).is_some() => 1,
|
||||
_ => 0,
|
||||
};
|
||||
let applies: [(&str, &str); 3] =
|
||||
[("Apply Color (,)", "color.applyColor"), ("Apply Gradient (.)", "color.applyGradient"), ("Apply None (/)", "color.applyNone")];
|
||||
// Apply Color shows the colour it applies.
|
||||
let last_color = app.ui.last_color.as_deref().and_then(|sw| doc.as_ref().and_then(|d| crate::widgets::swatch_colors(d, sw, 1.0).0));
|
||||
let paint_kind = |p: &egui::Painter, r: egui::Rect, k: usize| {
|
||||
let chip = egui::Rect::from_center_size(r.center(), vec2(13.0, 13.0));
|
||||
match k {
|
||||
0 => {
|
||||
p.rect_filled(chip, 0.0, last_color.unwrap_or(Color32::BLACK));
|
||||
}
|
||||
1 => crate::widgets::paint_chip(p, chip, None, Some((Color32::WHITE, Color32::BLACK))),
|
||||
_ => crate::widgets::paint_chip(p, chip, None, None),
|
||||
}
|
||||
p.rect_stroke(chip, 0.0, Stroke::new(0.5, t.border), StrokeKind::Outside);
|
||||
};
|
||||
let (row, _) = ui.allocate_exact_size(vec2(width, 21.0), Sense::hover());
|
||||
let well = egui::Rect::from_center_size(egui::pos2(row.min.x + width / 2.0, row.center().y), vec2(28.0, 21.0));
|
||||
let resp = ui.interact(well, ui.id().with("proxy_apply"), Sense::click()).on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Apply None (/)"));
|
||||
});
|
||||
ui.painter().rect(well, 2.0, t.well, Stroke::new(1.0, t.well_rim), StrokeKind::Outside);
|
||||
let chip = egui::Rect::from_center_size(well.center(), vec2(14.0, 14.0));
|
||||
crate::widgets::paint_chip(ui.painter(), chip, None, None);
|
||||
flyout_triangle(ui.painter(), well, t.icon);
|
||||
if resp.clicked() {
|
||||
let _ = app.run("color.applyNone", json!({}));
|
||||
}
|
||||
egui::Popup::context_menu(&resp).show(|ui| {
|
||||
for (label, sw) in [("Apply Color", "[Black]"), ("Apply None", "[None]")] {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
|
||||
let _ = crate::panels::apply_swatch(app, stroke_front, sw, None);
|
||||
ui.close();
|
||||
if cols == 2 {
|
||||
let pitch = 19.0;
|
||||
for (k, (tip, id)) in applies.into_iter().enumerate() {
|
||||
let c = egui::pos2(row.min.x + width / 2.0 + (k as f32 - 1.0) * pitch, row.center().y);
|
||||
let r = egui::Rect::from_center_size(c, vec2(18.0, 18.0));
|
||||
let resp = ui.interact(r, ui.id().with(("proxy_apply", k)), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, crate::i18n::tr(&app.ui.language, tip)));
|
||||
if k == front_kind {
|
||||
ui.painter().rect(r, 2.0, t.well, Stroke::new(1.0, t.well_rim), StrokeKind::Outside);
|
||||
}
|
||||
paint_kind(ui.painter(), r, k);
|
||||
if resp
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, tip));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run(id, json!({}));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let well = egui::Rect::from_center_size(egui::pos2(row.min.x + width / 2.0, row.center().y), vec2(28.0, 21.0));
|
||||
let (tip, id) = applies[front_kind];
|
||||
let resp = ui.interact(well, ui.id().with("proxy_apply"), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, crate::i18n::tr(&app.ui.language, tip)));
|
||||
let resp = resp.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, tip));
|
||||
});
|
||||
ui.painter().rect(well, 2.0, t.well, Stroke::new(1.0, t.well_rim), StrokeKind::Outside);
|
||||
paint_kind(ui.painter(), well, front_kind);
|
||||
flyout_triangle(ui.painter(), well, t.icon);
|
||||
if resp.clicked() {
|
||||
let _ = app.run(id, json!({}));
|
||||
}
|
||||
egui::Popup::context_menu(&resp).show(|ui| {
|
||||
for (tip, id) in applies {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, tip))).clicked() {
|
||||
let _ = app.run(id, json!({}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
separator(ui, &t);
|
||||
// Utility (frame options).
|
||||
// View Options (InDesign's: Frame Edges, Rulers, Guides, Smart Guides, Baseline Grid, Hidden
|
||||
// Characters, each checked when shown).
|
||||
ui.vertical_centered(|ui| {
|
||||
if tool_button(ui, "frame-options", false, crate::i18n::tr(&app.ui.language, "Text Frame Options")).clicked() {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("textFrameOptions", json!({})));
|
||||
}
|
||||
let resp = tool_button(ui, "view-options", false, crate::i18n::tr(&app.ui.language, "View Options"));
|
||||
flyout_triangle(ui.painter(), resp.rect.shrink2(vec2(2.0, 2.0)), t.icon);
|
||||
let open = resp.clicked();
|
||||
let popup = egui::Popup::menu(&resp).open_memory(open.then_some(egui::SetOpenCommand::Toggle));
|
||||
popup.show(|ui| {
|
||||
for (k, id) in
|
||||
["view.frameEdges", "view.rulers", "-", "view.guides", "view.smartGuides", "view.baselineGrid", "-", "view.hiddenCharacters"]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if id == "-" {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
let on = crate::menus::checked(app, id, &Value::Null).unwrap_or(false);
|
||||
let label = VIEW_OPTIONS[k];
|
||||
let sc = crate::menus::ui_label(id).and_then(|c| c.1).unwrap_or("");
|
||||
let text = format!("{} {}", if on { "✓" } else { " " }, crate::i18n::tr(&app.ui.language, label));
|
||||
if ui.add(egui::Button::new(crate::rtl::widget(ui, &text)).shortcut_text(sc)).clicked() {
|
||||
let _ = app.run(id, json!({}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
separator(ui, &t);
|
||||
// Screen Mode well with flyout.
|
||||
// Screen Mode: Normal and Preview (with Bleed, Slug and Presentation) side by side in two
|
||||
// columns, as InDesign's; one well with the five in its flyout in one column.
|
||||
let (row, _) = ui.allocate_exact_size(vec2(width, 22.0), Sense::hover());
|
||||
let mode = app.ui.screen_mode;
|
||||
let modes = [("Preview", "preview"), ("Bleed", "bleed"), ("Slug", "slug"), ("Presentation", "presentation")];
|
||||
if cols == 2 {
|
||||
let normal = egui::Rect::from_center_size(egui::pos2(row.min.x + width / 2.0 - 15.0, row.center().y), vec2(28.0, 21.0));
|
||||
let other = egui::Rect::from_center_size(egui::pos2(row.min.x + width / 2.0 + 15.0, row.center().y), vec2(28.0, 21.0));
|
||||
let is_normal = mode == crate::ScreenMode::Normal;
|
||||
for (r, on, icon) in [(normal, is_normal, "screen-normal"), (other, !is_normal, "screen-preview")] {
|
||||
if on {
|
||||
ui.painter().rect(r, 2.0, t.well, Stroke::new(1.0, t.well_rim), StrokeKind::Outside);
|
||||
}
|
||||
icons::paint(ui.painter(), egui::Rect::from_center_size(r.center(), vec2(15.0, 15.0)), icon, if on { t.text_strong } else { t.icon });
|
||||
}
|
||||
flyout_triangle(ui.painter(), other, t.icon);
|
||||
let tr = |s: &str| crate::i18n::tr(&app.ui.language, s).to_string();
|
||||
let (normal_tip, other_tip) = (tr("Normal"), format!("{} (W)", tr("Preview")));
|
||||
let resp = ui.interact(normal, ui.id().with("screen_normal"), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &normal_tip));
|
||||
if resp.on_hover_text(&normal_tip).clicked() {
|
||||
let _ = app.run("view.screenMode", json!({"mode": "normal"}));
|
||||
}
|
||||
let resp = ui.interact(other, ui.id().with("screen_mode"), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &other_tip));
|
||||
let resp = resp.on_hover_text(&other_tip);
|
||||
if resp.clicked() {
|
||||
let _ = app.run("view.screenMode", json!({"mode": "preview"}));
|
||||
}
|
||||
egui::Popup::context_menu(&resp).show(|ui| {
|
||||
for (label, m) in modes {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
|
||||
let _ = app.run("view.screenMode", json!({"mode": m}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
let well = egui::Rect::from_center_size(egui::pos2(row.min.x + width / 2.0, row.center().y), vec2(28.0, 21.0));
|
||||
let resp = ui.interact(well, ui.id().with("screen_mode"), Sense::click());
|
||||
ui.painter().rect(well, 2.0, t.well, Stroke::new(1.0, t.well_rim), StrokeKind::Outside);
|
||||
let preview = app.ui.screen_mode == crate::ScreenMode::Preview;
|
||||
let preview = mode == crate::ScreenMode::Preview;
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
egui::Rect::from_center_size(well.center(), vec2(15.0, 15.0)),
|
||||
@@ -370,7 +483,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
||||
let _ = app.run("view.togglePreview", json!({}));
|
||||
}
|
||||
egui::Popup::context_menu(&resp).show(|ui| {
|
||||
for (label, mode) in [("Normal", "normal"), ("Preview", "preview"), ("Bleed", "bleed"), ("Slug", "slug"), ("Presentation", "presentation")] {
|
||||
for (label, mode) in std::iter::once(("Normal", "normal")).chain(modes) {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
|
||||
let _ = app.run("view.screenMode", json!({"mode": mode}));
|
||||
ui.close();
|
||||
@@ -379,6 +492,9 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The View Options flyout's labels, by position (separators keep theirs empty).
|
||||
const VIEW_OPTIONS: [&str; 8] = ["Frame Edges", "Rulers", "", "Guides", "Smart Guides", "Baseline Grid", "", "Hidden Characters"];
|
||||
|
||||
/// The Object › Transform command a double-clicked tool opens (the Selection tool opens Move).
|
||||
fn transform_dialog_of(tool: &str) -> Option<&'static str> {
|
||||
match tool {
|
||||
@@ -444,6 +560,44 @@ pub(crate) fn toggle_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
|
||||
Ok(json!({"strokeFront": app.ui.stroke_front}))
|
||||
}
|
||||
|
||||
/// Apply Color (,): the colour applied last ([Black] at first) to the fill or stroke in front.
|
||||
pub(crate) fn apply_color(app: &mut DesignApp) -> Result<Value, String> {
|
||||
let sw = app.ui.last_color.clone().filter(|s| app.session.active().is_some_and(|d| d.doc.swatch(s).is_some()));
|
||||
crate::panels::apply_swatch(app, app.ui.stroke_front, sw.as_deref().unwrap_or("[Black]"), None)
|
||||
}
|
||||
|
||||
/// Apply Gradient (.): the gradient applied last, else the document's first gradient swatch, else
|
||||
/// a new white-to-black gradient (InDesign's default), on the fill or stroke in front.
|
||||
pub(crate) fn apply_gradient(app: &mut DesignApp) -> Result<Value, String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
let known = app
|
||||
.ui
|
||||
.last_gradient
|
||||
.clone()
|
||||
.filter(|s| designcraft_color::swatch::resolve_gradient(&st.doc.swatches, s).is_some())
|
||||
.or_else(|| st.doc.swatches.iter().find(|w| matches!(w.value, designcraft_color::SwatchValue::Gradient { .. })).map(|w| w.name.clone()));
|
||||
let stroke = app.ui.stroke_front;
|
||||
if let Some(sw) = known {
|
||||
return crate::panels::apply_swatch(app, stroke, &sw, None);
|
||||
}
|
||||
if crate::panels::colors_affect_text(app) {
|
||||
return Err("no gradient to apply yet: make one in the Gradient panel".into());
|
||||
}
|
||||
let fill = crate::panels::color_target(app).map(|t| t.0);
|
||||
let stops = json!([{"location": 0, "color": "#ffffff"}, {"location": 100, "color": "#000000"}]);
|
||||
let r = app.run("object.gradient", json!({"stops": stops}))?;
|
||||
let sw = r["swatch"].as_str().unwrap_or_default().to_string();
|
||||
app.ui.last_gradient = Some(sw.clone());
|
||||
if stroke {
|
||||
// The gradient was made on the fill: it goes to the stroke, the fill gets its swatch back.
|
||||
crate::panels::apply_swatch(app, true, &sw, None)?;
|
||||
if let Some(f) = fill {
|
||||
app.run("object.fill", json!({"swatch": f}))?;
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Apply None (/): to the fill or the stroke, whichever is in front of the proxy.
|
||||
pub(crate) fn apply_none(app: &mut DesignApp) -> Result<Value, String> {
|
||||
crate::panels::apply_swatch(app, app.ui.stroke_front, "[None]", None)
|
||||
@@ -550,6 +704,69 @@ mod tests {
|
||||
wheel(&mut h, panel.center(), vec2(0.0, -2000.0));
|
||||
assert_eq!((rect(&h, LAST_TOOL), rect(&h, FILL)), before);
|
||||
}
|
||||
#[test]
|
||||
fn the_tools_panel_lays_out_like_indesigns() {
|
||||
// Chris Nielsen's "The Adobe InDesign Toolbox" (InDesign 2020): Gradient Swatch and Gradient
|
||||
// Feather are tools of their own; in two columns Content Collector and Placer, Apply
|
||||
// Color / Gradient / None and Normal / Preview each get a button.
|
||||
for double in [false, true] {
|
||||
let h = test_window::open(app(double), vec2(1400.0, 900.0));
|
||||
let _ = rect(&h, "Gradient Swatch Tool (G)");
|
||||
let _ = rect(&h, "Gradient Feather Tool (Shift+G)");
|
||||
let _ = rect(&h, "View Options");
|
||||
let split = ["Content Placer Tool (B)", "Apply Color (,)", "Apply Gradient (.)", "Normal"];
|
||||
for label in split {
|
||||
assert_eq!(h.query_by_label(label).is_some(), double, "double column {double}: {label}");
|
||||
}
|
||||
let _ = rect(&h, "Content Collector Tool (B)");
|
||||
// One column: the Apply well names what is in front (no fill: Apply None).
|
||||
let _ = rect(&h, "Apply None (/)");
|
||||
if double {
|
||||
let (a, b) = (rect(&h, "Content Collector Tool (B)"), rect(&h, "Content Placer Tool (B)"));
|
||||
assert!((a.center().y - b.center().y).abs() < 1.0 && a.center().x < b.center().x, "side by side: {a:?} {b:?}");
|
||||
let (n, p) = (rect(&h, "Normal"), rect(&h, "Preview (W)"));
|
||||
assert!((n.center().y - p.center().y).abs() < 1.0, "Normal and Preview side by side");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_color_and_gradient_reuse_the_last_ones() {
|
||||
use serde_json::json;
|
||||
let mut app = app(true);
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let id = app.run("frame.create", json!({"rect": [72, 72, 200, 200], "content": "unassigned"})).unwrap()["id"].clone();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
let fill = |app: &crate::DesignApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
let it = st.doc.item(st.selection.items[0]).unwrap();
|
||||
(it.fill.swatch.clone(), it.stroke.swatch.clone())
|
||||
};
|
||||
// Apply Color: [Black] at first, then the colour last applied.
|
||||
app.run("color.applyColor", json!({})).unwrap();
|
||||
assert_eq!(fill(&app).0, "[Black]");
|
||||
crate::panels::apply_swatch(&mut app, false, "C=100 M=0 Y=0 K=0", None).unwrap();
|
||||
app.run("color.applyNone", json!({})).unwrap();
|
||||
assert_eq!(fill(&app).0, "[None]");
|
||||
app.run("color.applyColor", json!({})).unwrap();
|
||||
assert_eq!(fill(&app).0, "C=100 M=0 Y=0 K=0");
|
||||
// Apply Gradient: white to black when there's none yet, then that one again.
|
||||
app.run("color.applyGradient", json!({})).unwrap();
|
||||
let g = fill(&app).0;
|
||||
let doc = &app.session.active().unwrap().doc;
|
||||
let grad = designcraft_color::swatch::resolve_gradient(&doc.swatches, &g).expect("a gradient");
|
||||
assert_eq!((grad.stops[0].color.to_rgb(), grad.stops[1].color.to_rgb()), ([1.0; 3], [0.0; 3]));
|
||||
app.run("color.applyColor", json!({})).unwrap();
|
||||
// With the stroke in front, it goes to the stroke; the fill keeps its colour.
|
||||
app.ui.stroke_front = true;
|
||||
app.run("color.applyGradient", json!({})).unwrap();
|
||||
assert_eq!(fill(&app), ("C=100 M=0 Y=0 K=0".to_string(), g));
|
||||
// The shortcuts: , and .
|
||||
for (id, sc) in [("color.applyColor", ","), ("color.applyGradient", ".")] {
|
||||
assert_eq!(crate::menus::ui_label(id).and_then(|c| c.1), Some(sc));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Fill/Stroke proxy's keys run on the selection and stay out of text being typed.
|
||||
#[test]
|
||||
fn fill_stroke_proxy_keys() {
|
||||
|
||||
Reference in New Issue
Block a user