mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 20:26:57 -06:00
Merge #145: Right-click a ruler to pick its units; the vertical ruler uses the vertical units; Language in Properties › Character
This commit is contained in:
+2
-2
@@ -90,7 +90,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit ▸ Menus (`window.hideMenuItem`): hide menu items; menus with hidden items end in Show All Menu Items.
|
||||
- View ▸ Separations Preview (`view.separations`): one process plate as ink density, or areas over an ink limit in red (a GCR preview of the rendered view; spot plates and the flattener preview aren't there).
|
||||
- Separations Preview plates now come from each object's own colour (CMYK as authored; RGB and images through the colour settings, images via a cached 17³ table), so pure black shows only on the black plate.
|
||||
- Language (Character panel ▸ Language, `type.char {language}`): text in other languages isn't hyphenated or spell-checked with the English dictionaries, typographer's quotes follow the language (German „…“, French and Russian «…», Swiss, Spanish, Dutch, Nordic, Japanese and Traditional Chinese 「…」, Simplified Chinese “…”), the font's localized forms (OpenType `locl`: Turkish i, Bulgarian Cyrillic, Japanese / Chinese / Korean glyph variants) follow it too, and so does the fallback for CJK characters the font lacks (with Draw Missing Glyphs from Fallback Fonts on): Japanese, Simplified Chinese, Traditional Chinese and Korean each try their own installed system fonts (Japanese starts with the craft-fonts Japanese faces, Mincho first, when built with them; Hangul, kana and Bopomofo pick their language's fonts whatever the language). Language names from IDML keep their spelling; locale codes as InDesign writes them for some languages (`de_DE_2006`, `nl_NL_2005`; any `ll`, `ll_CC`, `ll-CC`) are their language and region for all of the above, and Chinese is also recognised in its common spellings ("Simplified Chinese", "Chinese (Traditional)", `zh_CN`, `zh-Hant`). Hyphenation dictionaries for other languages aren't bundled yet.
|
||||
- Language (Character panel ▸ Language, Properties ▸ Character, `type.char {language}`): text in other languages isn't hyphenated or spell-checked with the English dictionaries, typographer's quotes follow the language (German „…“, French and Russian «…», Swiss, Spanish, Dutch, Nordic, Japanese and Traditional Chinese 「…」, Simplified Chinese “…”), the font's localized forms (OpenType `locl`: Turkish i, Bulgarian Cyrillic, Japanese / Chinese / Korean glyph variants) follow it too, and so does the fallback for CJK characters the font lacks (with Draw Missing Glyphs from Fallback Fonts on): Japanese, Simplified Chinese, Traditional Chinese and Korean each try their own installed system fonts (Japanese starts with the craft-fonts Japanese faces, Mincho first, when built with them; Hangul, kana and Bopomofo pick their language's fonts whatever the language). Language names from IDML keep their spelling; locale codes as InDesign writes them for some languages (`de_DE_2006`, `nl_NL_2005`; any `ll`, `ll_CC`, `ll-CC`) are their language and region for all of the above, and Chinese is also recognised in its common spellings ("Simplified Chinese", "Chinese (Traditional)", `zh_CN`, `zh-Hant`). Hyphenation dictionaries for other languages aren't bundled yet.
|
||||
- Books (`book.new/open/add/remove/list/styleSource/paginate/syncStyles/exportPdf`, Book panel): `.dcbook` files, continuous page numbering, style sync from a style source, one merged PDF.
|
||||
- Track Changes (`changes.track/list/acceptAll/rejectAll`, Type ▸ Track Changes): typing is marked added (highlighted on screen), deletions are kept but hidden from layout and exports until accepted or rejected. Per-change accept/reject and authors aren't there yet.
|
||||
- Track Changes: per-change Accept / Reject (`changes.accept`, `changes.reject`) and a Track Changes panel listing every change.
|
||||
@@ -164,7 +164,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1059,6 +1059,18 @@ fn character_section(app: &mut DesignApp, ui: &mut Ui) {
|
||||
}) {
|
||||
let _ = app.run("type.char", json!({"attrs": {"tracking": v}}));
|
||||
}
|
||||
// Language: hyphenation, spelling and typographer's quotes follow it.
|
||||
ui.add_space(1.0);
|
||||
let lang = c["language"].as_str().unwrap_or("English: USA").to_string();
|
||||
let label = crate::i18n::tr(&app.ui.language, "Language");
|
||||
let resp = widgets::dropdown(ui, crate::i18n::tr(&app.ui.language, &lang), fw).on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, label);
|
||||
});
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, true, label));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(fw);
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| language_menu(app, ui, &lang));
|
||||
});
|
||||
if more_options(ui, &app.ui.language).clicked() {
|
||||
app.ui.open_panel = Some("character".into());
|
||||
}
|
||||
@@ -1201,6 +1213,17 @@ pub(crate) const LANGUAGES: &[&str] = &[
|
||||
"Hebrew",
|
||||
];
|
||||
|
||||
/// The Language list (Character panel, Properties ▸ Character): picking one sets the language of
|
||||
/// the selected text.
|
||||
fn language_menu(app: &mut DesignApp, ui: &mut Ui, cur: &str) {
|
||||
for l in LANGUAGES {
|
||||
if ui.selectable_label(*l == cur, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, l))).clicked() {
|
||||
let _ = app.run("type.char", json!({"attrs": {"language": l}}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(a) = text_attrs(app) else {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Select text or a text frame."));
|
||||
@@ -1220,13 +1243,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
egui::ComboBox::from_id_salt("char_language")
|
||||
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &lang)))
|
||||
.width(170.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for l in LANGUAGES {
|
||||
if ui.selectable_label(*l == lang, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, l))).clicked() {
|
||||
let _ = app.run("type.char", json!({"attrs": {"language": l}}));
|
||||
}
|
||||
}
|
||||
});
|
||||
.show_ui(ui, |ui| language_menu(app, ui, &lang));
|
||||
});
|
||||
// Digits (World-Ready): shown for right-to-left paragraphs or once set.
|
||||
let digits = c["digits"].as_str().unwrap_or("default").to_string();
|
||||
@@ -2109,3 +2126,42 @@ fn variable_font_axes(app: &mut DesignApp, ui: &mut egui::Ui, family: &str, styl
|
||||
let _ = app.run("type.char", json!({"attrs": {"fontStyle": format!("{base} {{{spec}}}")}}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn language(app: &mut DesignApp) -> Value {
|
||||
text_attrs(app).unwrap()["chars"]["language"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_character_section_sets_the_language() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "bunun", "caret": true})).unwrap();
|
||||
app.run("edit.selectAll", json!({})).unwrap();
|
||||
assert_ne!(language(&mut app), "French");
|
||||
let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
// The panel's fonts are installed before its first frame.
|
||||
let id = egui::Id::new("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(id)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(id, true));
|
||||
return;
|
||||
}
|
||||
show(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Language").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("French").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(language(h.state_mut()), "French");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user