mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 14:47:05 -06:00
UI: right-clicking a ruler picks that ruler's units, and the vertical ruler measures in the vertical units (#133)
This commit is contained in:
+1
-1
@@ -161,7 +161,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient`; the vector round-trips through IDML.
|
||||
- Document Setup (⌥⌘P): intent, number of pages, start page # (an even start begins with a left page), facing pages, size presets and orientation, bleed and slug per edge; `layout.documentSetup {}` reports the setup.
|
||||
- Stroke panel: cap, join, miter limit, alignment, type, Start/End arrowheads (12 kinds, drawn on screen and in PDF; the path is shortened under pointed heads) and gap colour under dashes and dots.
|
||||
- Preferences (⌘K: General, Type, Units & Increments, Grids, Guides & Pasteboard, Display Performance) backed by `prefs.set` (application) and `document.preferences` (units, increments, grids — undoable, saved with the document); scaling applies to content and scales stroke weights (Include Stroke Weight), and X/Y/W/H measure the stroke's outer edge (Dimensions Include Stroke Weight). Quick Apply (⌘Return) finds paragraph, character and object styles as well as commands.
|
||||
- Preferences (⌘K: General, Type, Units & Increments, Grids, Guides & Pasteboard, Display Performance) backed by `prefs.set` (application) and `document.preferences` (units, increments, grids — undoable, saved with the document; right-click a ruler to pick its units, the horizontal and vertical rulers each have their own); scaling applies to content and scales stroke weights (Include Stroke Weight), and X/Y/W/H measure the stroke's outer edge (Dimensions Include Stroke Weight). Quick Apply (⌘Return) finds paragraph, character and object styles as well as commands.
|
||||
- Parent item overrides: Cmd+Shift-click a parent item on a page (or Override All Parent Page Items, ⌥⇧⌘L) makes an editable local copy that hides the parent's; Remove All Local Overrides and Detach All Objects from Parent in the Pages panel menu.
|
||||
- Place PDF: placed PDF pages are sized by their crop box, drawn on screen through hayro (with mip levels) and embedded in exported PDFs as vector form XObjects (krilla), `<PDF>` in IDML.
|
||||
- Menus expose the engine's features (TOC, numbering & sections, text variables, hyperlinks, cross-references, footnotes, step and repeat, spelling, fitting/content, effects, Data Merge, Preflight, EPUB); any menu command with parameters gets a dialog generated from its parameter documentation.
|
||||
|
||||
@@ -318,6 +318,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
draw_tool_overlays(app, &painter, &xf);
|
||||
if rulers {
|
||||
draw_rulers(app, ui, full, rect, &xf, &doc, &layout, &t);
|
||||
ruler_units_menus(app, &resp, full, rect, &doc);
|
||||
}
|
||||
if let Some(r) = sel_rect
|
||||
&& !preview
|
||||
@@ -1247,8 +1248,7 @@ fn draw_tool_overlays(app: &mut DesignApp, painter: &egui::Painter, xf: &Xf) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_rulers(app: &DesignApp, ui: &egui::Ui, full: Rect, rect: Rect, xf: &Xf, doc: &Document, layout: &CanvasLayout, t: &Tokens) {
|
||||
let painter = ui.painter_at(full);
|
||||
let top = Rect::from_min_max(pos2(rect.min.x, full.min.y), pos2(full.max.x, rect.min.y));
|
||||
let left = Rect::from_min_max(pos2(full.min.x, rect.min.y), pos2(rect.min.x, full.max.y));
|
||||
let (top, left) = ruler_rects(full, rect);
|
||||
painter.rect_filled(top, 0.0, t.ruler);
|
||||
painter.rect_filled(left, 0.0, t.ruler);
|
||||
painter.rect_filled(Rect::from_min_max(full.min, rect.min), 0.0, t.ruler);
|
||||
@@ -1262,11 +1262,11 @@ fn draw_rulers(app: &DesignApp, ui: &egui::Ui, full: Rect, rect: Rect, xf: &Xf,
|
||||
let Some(i) = current_slot(app, layout) else { return };
|
||||
let slot = &layout.slots[i];
|
||||
let origin = Point::new(slot.bounds.x0, slot.bounds.y0);
|
||||
let unit = doc.settings.horizontal_units;
|
||||
let (major, sub) = unit.ruler_ticks(xf.zoom);
|
||||
let font = egui::FontId::proportional(9.0);
|
||||
let tick = Stroke::new(1.0, t.ruler_tick);
|
||||
// Horizontal.
|
||||
let unit = doc.settings.horizontal_units;
|
||||
let (major, sub) = unit.ruler_ticks(xf.zoom);
|
||||
let c0 = xf.to_canvas(top.min).x - origin.x;
|
||||
let c1 = xf.to_canvas(pos2(top.max.x, 0.0)).x - origin.x;
|
||||
let mut k = (c0 / major).floor() as i64;
|
||||
@@ -1291,6 +1291,8 @@ fn draw_rulers(app: &DesignApp, ui: &egui::Ui, full: Rect, rect: Rect, xf: &Xf,
|
||||
k += 1;
|
||||
}
|
||||
// Vertical.
|
||||
let unit = doc.settings.vertical_units;
|
||||
let (major, sub) = unit.ruler_ticks(xf.zoom);
|
||||
let c0 = xf.to_canvas(pos2(0.0, left.min.y)).y - origin.y;
|
||||
let c1 = xf.to_canvas(pos2(0.0, left.max.y)).y - origin.y;
|
||||
let mut k = (c0 / major).floor() as i64;
|
||||
@@ -1325,6 +1327,43 @@ fn draw_rulers(app: &DesignApp, ui: &egui::Ui, full: Rect, rect: Rect, xf: &Xf,
|
||||
}
|
||||
}
|
||||
|
||||
/// The horizontal (top) and vertical (left) rulers around the view `rect` in `full`.
|
||||
fn ruler_rects(full: Rect, rect: Rect) -> (Rect, Rect) {
|
||||
(
|
||||
Rect::from_min_max(pos2(rect.min.x, full.min.y), pos2(full.max.x, rect.min.y)),
|
||||
Rect::from_min_max(pos2(full.min.x, rect.min.y), pos2(rect.min.x, full.max.y)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Right-clicking a ruler opens a menu of units for that ruler.
|
||||
fn ruler_units_menus(app: &mut DesignApp, resp: &egui::Response, full: Rect, rect: Rect, doc: &Document) {
|
||||
let (top, left) = ruler_rects(full, rect);
|
||||
let at = resp.interact_pointer_pos();
|
||||
for (ruler, key, cur) in [(top, "horizontalUnits", doc.settings.horizontal_units), (left, "verticalUnits", doc.settings.vertical_units)] {
|
||||
let open = if resp.secondary_clicked() {
|
||||
Some(egui::SetOpenCommand::Bool(at.is_some_and(|p| ruler.contains(p))))
|
||||
} else if resp.clicked() {
|
||||
Some(egui::SetOpenCommand::Bool(false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
egui::Popup::context_menu(resp).id(resp.id.with(("ruler_units", key))).open_memory(open).show(|ui| ruler_units_menu(app, ui, key, cur));
|
||||
}
|
||||
}
|
||||
|
||||
/// Every unit, the current one checked; picking one sets `key` (`horizontalUnits` or
|
||||
/// `verticalUnits`) through `document.preferences`.
|
||||
fn ruler_units_menu(app: &mut DesignApp, ui: &mut egui::Ui, key: &str, cur: designcraft_geom::Unit) {
|
||||
for u in designcraft_geom::Unit::ALL {
|
||||
let label = crate::i18n::tr(&app.ui.language, u.label());
|
||||
let text = if u == cur { format!("✓ {label}") } else { format!(" {label}") };
|
||||
if ui.button(crate::rtl::widget(ui, text)).clicked() {
|
||||
let _ = app.run("document.preferences", json!({ key: u }));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_tick(v: f64) -> String {
|
||||
let r = v.round();
|
||||
if (v - r).abs() < 1e-6 { format!("{}", r as i64) } else { format!("{v:.1}") }
|
||||
@@ -1726,3 +1765,72 @@ fn guide_drag(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, rec
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use designcraft_geom::Unit;
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn harness(app: DesignApp) -> Harness<'static, DesignApp> {
|
||||
let mut h = Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn right_click(h: &mut Harness<'_, DesignApp>, p: Pos2) {
|
||||
h.hover_at(p);
|
||||
h.step();
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() });
|
||||
}
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn units(h: &Harness<'_, DesignApp>) -> (Unit, Unit) {
|
||||
let s = &h.state().session.doc().unwrap().doc.settings;
|
||||
(s.horizontal_units, s.vertical_units)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_clicking_a_ruler_sets_that_rulers_units() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let mut h = harness(app);
|
||||
let rect = h.state().canvas_rect.unwrap();
|
||||
assert_eq!(units(&h), (Unit::Picas, Unit::Picas));
|
||||
// Vertical ruler: the current unit is checked; picking one sets only the vertical units.
|
||||
right_click(&mut h, pos2(rect.min.x - RULER / 2.0, rect.center().y));
|
||||
h.get_by_label("✓ Picas");
|
||||
h.get_by_label(" Millimeters").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(units(&h), (Unit::Picas, Unit::Millimeters));
|
||||
// Horizontal ruler.
|
||||
right_click(&mut h, pos2(rect.center().x, rect.min.y - RULER / 2.0));
|
||||
h.get_by_label("✓ Picas");
|
||||
h.get_by_label(" Inches").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(units(&h), (Unit::Inches, Unit::Millimeters));
|
||||
// The page and the ruler corner open no units menu.
|
||||
for p in [rect.center(), rect.min - vec2(RULER / 2.0, RULER / 2.0)] {
|
||||
right_click(&mut h, p);
|
||||
assert!(h.query_by_label(" Points").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rulers_open_no_units_menu_without_a_document() {
|
||||
let mut h = harness(DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()));
|
||||
let Some(rect) = h.state().canvas_rect else { return };
|
||||
right_click(&mut h, pos2(rect.center().x, rect.min.y - RULER / 2.0));
|
||||
assert!(h.query_by_label(" Points").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user