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:
aiongg
2026-10-08 10:10:41 -04:00
parent 5382fd4fb7
commit 8ddd20d95d
2 changed files with 113 additions and 5 deletions
+1 -1
View File
@@ -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.
+112 -4
View File
@@ -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());
}
}