mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M11.4: text-thread connectors on selected threaded frames + View → Hide/Show Text Threads, Hide/Show Gradient Annotator, Type → Show Hidden Characters (spaces, ¶, #), Effect → Last Effect… (reopens the last effect's dialog)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dcc5a006a2
commit
55e24d6622
4 files changed
+139
-8
No files matched your search
@@ -97,7 +97,10 @@ impl NativeMenu {
|
||||
) || cmd.starts_with("file.openRecent")
|
||||
|| cmd.starts_with("type.recentFont")
|
||||
|| cmd.starts_with("view.goto")
|
||||
|| cmd == "view.cornerWidget"
|
||||
|| matches!(
|
||||
cmd.as_str(),
|
||||
"view.cornerWidget" | "view.textThreads" | "view.gradientAnnotator" | "effect.last" | "type.hiddenCharacters"
|
||||
)
|
||||
{
|
||||
i.set_text(menus::dynamic_label(app, cmd, ""));
|
||||
}
|
||||
|
||||
@@ -267,10 +267,19 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if app.ui.view.edges {
|
||||
hover_highlight(app, &painter, &xf);
|
||||
selection_overlay(app, &painter, &xf);
|
||||
if app.ui.view.text_threads {
|
||||
thread_overlay(app, &painter, &xf);
|
||||
}
|
||||
if app.ui.view.hidden_chars {
|
||||
hidden_chars_overlay(app, &painter, &xf);
|
||||
}
|
||||
}
|
||||
let view_info = app.view_info();
|
||||
let overlays = app.session.overlays(view_info);
|
||||
draw_overlays(&painter, &xf, &overlays, &t);
|
||||
// View → Hide Gradient Annotator hides the Gradient tool's annotator.
|
||||
if app.session.tool_id() != "gradient" || app.ui.view.gradient_annotator {
|
||||
let overlays = app.session.overlays(view_info);
|
||||
draw_overlays(&painter, &xf, &overlays, &t);
|
||||
}
|
||||
|
||||
if app.ui.view.rulers && app.ui.screen_mode < 3 {
|
||||
rulers(ui, full, &xf, app.hover_doc, &t);
|
||||
@@ -679,6 +688,67 @@ fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size:
|
||||
}
|
||||
}
|
||||
|
||||
/// Type → Show Hidden Characters: spaces as dots, ¶ at paragraph ends, # at the end of a story.
|
||||
fn hidden_chars_overlay(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let clip = p.clip_rect();
|
||||
let color = Color32::from_rgb(0x4f, 0x9d, 0xff);
|
||||
let font = egui::FontId::proportional(((10.0 * xf.zoom) as f32).clamp(7.0, 18.0));
|
||||
st.doc.walk(|n| {
|
||||
let NodeKind::Text(tx) = &n.kind else { return };
|
||||
if !n.visible || n.geometric_bounds().is_none_or(|b| !xf.rect_to_screen(b).intersects(clip)) {
|
||||
return;
|
||||
}
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), tx);
|
||||
if lay.on_path {
|
||||
return;
|
||||
}
|
||||
let text = tx.plain_text();
|
||||
let to = |q: Point| xf.to_screen(tx.xf * q);
|
||||
for g in &lay.glyphs {
|
||||
if text.get(g.byte..).is_some_and(|s| s.starts_with(' ')) {
|
||||
p.circle_filled(to(Point::new(g.origin.x + g.advance / 2.0, g.origin.y - 3.0)), 1.2, color);
|
||||
}
|
||||
}
|
||||
for (i, line) in lay.lines.iter().enumerate() {
|
||||
let last = i + 1 == lay.lines.len();
|
||||
let breaks = text.get(line.end..).is_some_and(|s| s.starts_with('\n'));
|
||||
let (glyph, at) = if last && line.end >= text.len() {
|
||||
("#", true)
|
||||
} else if breaks {
|
||||
("¶", true)
|
||||
} else {
|
||||
("", false)
|
||||
};
|
||||
if at {
|
||||
p.text(to(Point::new(line.x1 + 1.0, line.baseline)), egui::Align2::LEFT_BOTTOM, glyph, font.clone(), color);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Text threads of the selected frames: a line from each frame's out port (bottom right) to the
|
||||
/// next frame's in port (top left), like Illustrator's thread indicators.
|
||||
fn thread_overlay(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
for thread in &st.doc.text_threads {
|
||||
if !thread.iter().any(|id| st.selection.objects.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
let color = c32(st.doc.layer_color(thread[0]));
|
||||
let frames: Vec<vectorcraft_geom::Rect> = thread.iter().filter_map(|id| st.doc.node(*id).and_then(|n| n.geometric_bounds())).collect();
|
||||
for w in frames.windows(2) {
|
||||
let (out, inp) = (xf.to_screen(Point::new(w[0].x1, w[0].y1)), xf.to_screen(Point::new(w[1].x0, w[1].y0)));
|
||||
p.line_segment([out, inp], Stroke::new(1.0, color));
|
||||
for port in [out, inp] {
|
||||
p.rect_filled(egui::Rect::from_center_size(port, egui::vec2(7.0, 7.0)), 0.0, Color32::WHITE);
|
||||
p.rect_stroke(egui::Rect::from_center_size(port, egui::vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, color), egui::StrokeKind::Inside);
|
||||
p.line_segment([port - egui::vec2(2.0, 0.0), port + egui::vec2(2.0, 0.0)], Stroke::new(1.0, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_overlay(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let tool = app.session.tool_id();
|
||||
|
||||
@@ -102,6 +102,10 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"),
|
||||
("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"),
|
||||
("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"),
|
||||
("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"),
|
||||
("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"),
|
||||
("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"),
|
||||
("effect.last", "Last Effect…", "Cmd+Alt+Shift+E", "{} open the dialog of the last effect applied"),
|
||||
("view.zoomIn", "Zoom In", "Cmd+=", "{}"),
|
||||
("view.zoomOut", "Zoom Out", "Cmd+-", "{}"),
|
||||
("view.fitArtboard", "Fit Artboard in Window", "Cmd+0", "{}"),
|
||||
@@ -282,6 +286,21 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
}
|
||||
}
|
||||
"view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel),
|
||||
"view.textThreads" => flag(&mut app.ui.view.text_threads),
|
||||
"type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars),
|
||||
"view.gradientAnnotator" => flag(&mut app.ui.view.gradient_annotator),
|
||||
"effect.last" => match app.last_effect.clone() {
|
||||
Some((e, params)) => {
|
||||
let label = vectorcraft_effects::effect_info(&e).map(|i| i.label.trim_end_matches('…').to_string()).unwrap_or_else(|| e.clone());
|
||||
let mut fields = params.as_object().cloned().unwrap_or_default();
|
||||
fields.insert("__effect".into(), json!(e));
|
||||
fields.insert("__label".into(), json!(label));
|
||||
fields.insert("preview".into(), json!(true));
|
||||
app.ui.dialog = Some(crate::state::Dialog { kind: "effect".into(), fields });
|
||||
Ok(Value::Null)
|
||||
}
|
||||
None => Err("no effect applied yet".into()),
|
||||
},
|
||||
id if id.starts_with("type.recentFont") => {
|
||||
let n: usize = id["type.recentFont".len()..].parse().unwrap_or(0);
|
||||
match n.checked_sub(1).and_then(|i| app.ui.recent_fonts.get(i)).cloned() {
|
||||
@@ -513,6 +532,13 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
|
||||
"window.workspace.reset" => format!("Reset {}", app.ui.workspace),
|
||||
"view.edges" => if v.edges { "Hide Edges" } else { "Show Edges" }.into(),
|
||||
"view.cornerWidget" => if v.corner_widgets { "Hide Corner Widget" } else { "Show Corner Widget" }.into(),
|
||||
"view.textThreads" => if v.text_threads { "Hide Text Threads" } else { "Show Text Threads" }.into(),
|
||||
"type.hiddenCharacters" => if v.hidden_chars { "Hide Hidden Characters" } else { "Show Hidden Characters" }.into(),
|
||||
"view.gradientAnnotator" => if v.gradient_annotator { "Hide Gradient Annotator" } else { "Show Gradient Annotator" }.into(),
|
||||
"effect.last" => match &app.last_effect {
|
||||
Some((e, _)) => format!("Last Effect: {}", vectorcraft_effects::effect_info(e).map(|i| i.label).unwrap_or(e.as_str())),
|
||||
None => label.into(),
|
||||
},
|
||||
id if id.starts_with("type.recentFont") => {
|
||||
let n: usize = id["type.recentFont".len()..].parse().unwrap_or(0);
|
||||
n.checked_sub(1).and_then(|i| app.ui.recent_fonts.get(i)).cloned().unwrap_or_else(|| "—".into())
|
||||
@@ -605,7 +631,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||
id["view.goto".len()..].parse::<usize>().is_ok_and(|n| n >= 1 && app.session.active().is_some_and(|d| n <= d.doc.views.len()))
|
||||
}
|
||||
"effect.dialog" | "ui.recolorDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||
"effect.applyLast" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||
"effect.applyLast" | "effect.last" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()),
|
||||
"file.export.pdf" => app.session.active().is_some(),
|
||||
_ => true,
|
||||
}
|
||||
@@ -976,7 +1002,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
Sep,
|
||||
c("Fill with Placeholder Text", "type.fillPlaceholder"),
|
||||
Sep,
|
||||
todos("Show Hidden Characters", "Cmd+Alt+I"),
|
||||
c("Show Hidden Characters", "type.hiddenCharacters"),
|
||||
sub("Type Orientation", vec![todo("Horizontal"), todo("Vertical")]),
|
||||
],
|
||||
),
|
||||
@@ -1086,8 +1112,8 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
sub("Rulers", vec![c("Show Rulers", "view.rulers"), todos("Change to Global Rulers", "Cmd+Alt+R"), todo("Show Video Rulers")]),
|
||||
c("Hide Bounding Box", "view.boundingBox"),
|
||||
c("Show Transparency Grid", "view.transparencyGrid"),
|
||||
todos("Hide Text Threads", "Cmd+Shift+Y"),
|
||||
todos("Hide Gradient Annotator", "Cmd+Alt+G"),
|
||||
c("Hide Text Threads", "view.textThreads"),
|
||||
c("Hide Gradient Annotator", "view.gradientAnnotator"),
|
||||
c("Hide Corner Widget", "view.cornerWidget"),
|
||||
Sep,
|
||||
sub(
|
||||
@@ -1552,7 +1578,7 @@ fn effect_menu() -> Vec<Item> {
|
||||
let cat = vectorcraft_effects::effect_catalog();
|
||||
let mut out = vec![
|
||||
c("Apply Last Effect", "effect.applyLast"),
|
||||
todos("Last Effect", "Cmd+Alt+Shift+E"),
|
||||
c("Last Effect…", "effect.last"),
|
||||
Sep,
|
||||
c("Document Raster Effects Settings…", "document.rasterEffectsSettings"),
|
||||
Sep,
|
||||
@@ -1602,6 +1628,29 @@ fn effect_menu() -> Vec<Item> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn last_effect_dialog_and_view_toggles() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
assert!(!enabled(&app, "effect.last"));
|
||||
app.last_effect = Some(("distort.roughen".into(), json!({"size": 9})));
|
||||
assert!(enabled(&app, "effect.last"));
|
||||
assert_eq!(dynamic_label(&app, "effect.last", "Last Effect…"), "Last Effect: Roughen…");
|
||||
app.run("effect.last", json!({})).unwrap();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.kind.as_str(), d.str("__effect"), d.f64("size", 0.0)), ("effect", "distort.roughen".to_string(), 9.0));
|
||||
for (id, on, off) in [
|
||||
("view.textThreads", "Hide Text Threads", "Show Text Threads"),
|
||||
("view.gradientAnnotator", "Hide Gradient Annotator", "Show Gradient Annotator"),
|
||||
("type.hiddenCharacters", "Show Hidden Characters", "Hide Hidden Characters"),
|
||||
] {
|
||||
assert_eq!(dynamic_label(&app, id, ""), on);
|
||||
app.run(id, json!({})).unwrap();
|
||||
assert_eq!(dynamic_label(&app, id, ""), off);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_views_store_and_restore_the_view() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -121,6 +121,12 @@ pub struct ViewFlags {
|
||||
pub corner_widgets: bool,
|
||||
/// View → Snap to Pixel.
|
||||
pub snap_to_pixel: bool,
|
||||
/// View → Show/Hide Text Threads.
|
||||
pub text_threads: bool,
|
||||
/// View → Show/Hide Gradient Annotator.
|
||||
pub gradient_annotator: bool,
|
||||
/// Type → Show Hidden Characters.
|
||||
pub hidden_chars: bool,
|
||||
}
|
||||
|
||||
impl Default for ViewFlags {
|
||||
@@ -141,6 +147,9 @@ impl Default for ViewFlags {
|
||||
trim_view: false,
|
||||
corner_widgets: true,
|
||||
snap_to_pixel: false,
|
||||
text_threads: true,
|
||||
gradient_annotator: true,
|
||||
hidden_chars: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user