UI: add Chinese and Arabic localization with RTL-safe interface text
Extend interface language selection with Chinese and Arabic translations across menus, dialogs, panels, tooltips, and common application controls. Keep English fallback for untranslated strings, return borrowed translation text, and validate translation key uniqueness and language switching. Bundle redistributable Noto CJK and Arabic fonts with OFL license files and ASSETS.md attribution so native and WASM builds have portable glyph coverage. Register Arabic regular and semibold families while retaining existing font fallbacks for proportional and monospace controls. Render Arabic labels using Unicode bidirectional runs and contextual shaping. Wrap logical text at grapheme boundaries before ordering each visual line, mirror neutral punctuation, preserve logical source text for accessibility, and handle truncation, empty lines, and mixed Latin text and numbers. Keep ASCII labels on the normal rendering path. Disable selection on shaped RTL labels because egui's default label cursor mapping assumes LTR text. Adapt welcome controls, menu ordering, popup alignment, and new-document rows for RTL layout. Bound popup geometry and avoid unsupported RTL grid behavior; keep numeric inputs LTR. Isolate user-provided document names from Latin application and zoom metadata. Interface language changes leave document binding, paragraph direction, editable text, and engine state unchanged. Add regressions for contextual Arabic joining, bidirectional punctuation, mixed document names, logical wrapping, truncation, language switching, and finite widget geometry. Update the roadmap with localization coverage. Apply equivalent chunk-iteration and PDF Option/borrow simplifications needed by the current Rust toolchain's strict Clippy checks. Validation: - cargo xtask ci: fmt, Clippy, native tests, assets, and layers passed; native tests total 517 passed and 2 ignored. - UI regression suite: 26 passed. - Headless UI screenshots verified Arabic welcome/new-document creation, menus, mixed-script document names, and switching back to Chinese/English. - Default WASM gate is blocked by this host's missing precompiled wasm32 standard library. All 15 WASM crates pass using the installed Rust sources: RUSTC_BOOTSTRAP=1 CARGO_UNSTABLE_BUILD_STD=std,panic_abort target/debug/xtask wasm.
This commit is contained in:
parent
e8d6cedfea
commit
9d85777546
|
|
@ -1111,6 +1111,9 @@ dependencies = [
|
|||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"unicode-bidi",
|
||||
"unicode-bidi-mirroring",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ log = "0.4"
|
|||
proptest = "1"
|
||||
rayon = "1"
|
||||
unicode-bidi = "0.3"
|
||||
unicode-bidi-mirroring = "0.4"
|
||||
unicode-segmentation = "1"
|
||||
regex = { version = "1", default-features = false, features = ["std", "unicode", "perf"] }
|
||||
|
||||
# text / rendering / formats
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
|
||||
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. Interactive PDF export (`file.exportPdf {media: true}`, File ▸ Export PDF (Interactive)) embeds each file and plays it in a Screen annotation through a rendition action (controls, loop, play on page load). No in-app playback.
|
||||
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left.
|
||||
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish and Japanese menu titles, common menu items and panel names (our own translations); dialogs and the rest of the UI stay English. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Japanese glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic for the UI, Mincho for documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts.
|
||||
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish, Japanese, Simplified Chinese and Arabic menu titles, common menu items and panel names (our own translations). Common dialog, panel and tooltip labels are also localized, with English fallback for untranslated strings. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Arabic labels use Unicode bidi run ordering and contextual shaping; welcome and new-document form layouts follow RTL direction independently of document binding. CJK and Arabic glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic and Noto Sans CJK SC for the UI (Chinese forms first in the Chinese interface), Noto Sans Arabic, and Mincho for Japanese documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts.
|
||||
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
|
||||
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
|
||||
- EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ designcraft-compose = { workspace = true }
|
|||
designcraft-render = { workspace = true }
|
||||
designcraft-tools = { workspace = true }
|
||||
designcraft-engine = { workspace = true }
|
||||
unicode-bidi = { workspace = true }
|
||||
unicode-bidi-mirroring = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ pub fn discord_button(ui: &mut egui::Ui, label: &str, size: egui::Vec2) -> bool
|
|||
ui.painter().rect_filled(r, size.y / 2.0, fill);
|
||||
let icon = size.y * 0.5;
|
||||
let font = semibold((size.y * 0.42).max(11.0));
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), font, Color32::WHITE);
|
||||
let galley = crate::rtl::plain(ui.ctx(), label, font, Color32::WHITE);
|
||||
let total = icon + 8.0 + galley.size().x;
|
||||
let x0 = r.center().x - total / 2.0;
|
||||
paint_chat_icon(ui.painter(), pos2(x0 + icon / 2.0, r.center().y), icon, Color32::WHITE);
|
||||
|
|
@ -109,17 +109,35 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
|||
let (r, _) = ui.allocate_exact_size(vec2(72.0, 72.0), Sense::hover());
|
||||
paint_mark(ui, r, BRAND);
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("DesignCraft").font(semibold(26.0)).color(t.text_strong));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "DesignCraft")).font(semibold(26.0)).color(t.text_strong),
|
||||
));
|
||||
ui.label(egui::RichText::new(format!("Version {}", env!("CARGO_PKG_VERSION"))).size(12.0).color(t.text_dim));
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Page layout for print and screen — fast, open and scriptable.").size(13.0).color(t.text));
|
||||
ui.label(egui::RichText::new("Part of the ArtCraft suite of open creative tools.").size(12.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Page layout for print and screen — fast, open and scriptable."))
|
||||
.size(13.0)
|
||||
.color(t.text),
|
||||
));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Part of the ArtCraft suite of open creative tools."))
|
||||
.size(12.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
ui.add_space(16.0);
|
||||
if discord_button(ui, "Join the ArtCraft Discord", vec2(300.0, 40.0)) {
|
||||
open = Some("discord");
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("discord.gg/artcraft — help, feedback and show-and-tell").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "discord.gg/artcraft — help, feedback and show-and-tell"))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
ui.add_space(14.0);
|
||||
ui.horizontal(|ui| {
|
||||
// Centre the row of links.
|
||||
|
|
@ -137,7 +155,10 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
|||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("More ArtCraft apps").font(semibold(12.0)).color(t.text_strong));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "More ArtCraft apps")).font(semibold(12.0)).color(t.text_strong),
|
||||
));
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
let pad = ui.spacing().button_padding.x * 2.0 + ui.spacing().item_spacing.x;
|
||||
|
|
@ -157,9 +178,12 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
|||
}
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
ui.label(egui::RichText::new("Open source. No telemetry.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Open source. No telemetry.")).size(11.0).color(t.text_dim),
|
||||
));
|
||||
ui.add_space(8.0);
|
||||
if ui.button(" Close ").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, " Close "))).clicked() {
|
||||
app.ui.about = false;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,12 +61,12 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let _ = app.run("window.resetWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("New Workspace…").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
|
||||
let _ = app.run("window.newWorkspace", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if !customs.is_empty() {
|
||||
ui.menu_button("Delete Workspace", |ui| {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")), |ui| {
|
||||
for w in &customs {
|
||||
if ui.button(w).clicked() {
|
||||
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
|
||||
|
|
@ -90,11 +90,11 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
// Centred title.
|
||||
let title = match app.session.active() {
|
||||
Some(d) => format!("DesignCraft — {}", d.title()),
|
||||
Some(d) => format!("DesignCraft — {}", crate::rtl::isolate(&d.title())),
|
||||
None => "DesignCraft".to_string(),
|
||||
};
|
||||
// Centred between the menus and the right-hand controls; left out when it won't fit.
|
||||
let galley = ui.painter().layout_no_wrap(title, egui::FontId::proportional(11.5), t.text);
|
||||
let galley = crate::rtl::plain(ui.ctx(), &title, egui::FontId::proportional(11.5), t.text);
|
||||
let w = galley.size().x;
|
||||
let cx = full.center().x;
|
||||
if cx - w / 2.0 > menus_end + 12.0 && cx + w / 2.0 < tools_start - 12.0 {
|
||||
|
|
@ -209,15 +209,33 @@ fn control_object(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
if icons::button(ui, "tool-rotate", 20.0, false, "Rotate 90° Counterclockwise").clicked() {
|
||||
let _ = app.run("transform.rotate", json!({"angle": 90}));
|
||||
}
|
||||
if ui.small_button("⇋").on_hover_text("Flip Horizontal").clicked() {
|
||||
if ui
|
||||
.small_button("⇋")
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Flip Horizontal"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("transform.flip", json!({"axis": "horizontal"}));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("↻").on_hover_text("Rotate 90° Clockwise").clicked() {
|
||||
if ui
|
||||
.small_button("↻")
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Rotate 90° Clockwise"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("transform.rotate", json!({"angle": -90}));
|
||||
}
|
||||
if ui.small_button("⇵").on_hover_text("Flip Vertical").clicked() {
|
||||
if ui
|
||||
.small_button("⇵")
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Flip Vertical"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("transform.flip", json!({"axis": "vertical"}));
|
||||
}
|
||||
});
|
||||
|
|
@ -429,8 +447,8 @@ pub fn doc_tabs(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let is_active = Some(i) == active;
|
||||
// `*` = unsaved; the view-mode suffix only in Preview (InDesign shows its GPU mode there).
|
||||
let suffix = if is_active && app.ui.screen_mode == crate::ScreenMode::Preview { " [Preview]" } else { "" };
|
||||
let label = format!("{}{} @ {:.0}%{suffix}", if *dirty { "*" } else { "" }, title, zoom * 100.0);
|
||||
let galley = ui.painter().layout_no_wrap(label, semibold(11.5), if is_active { t.text_strong } else { t.text_dim });
|
||||
let label = format!("{}{} @ {:.0}%{suffix}", if *dirty { "*" } else { "" }, crate::rtl::isolate(title), zoom * 100.0);
|
||||
let galley = crate::rtl::plain(ui.ctx(), &label, semibold(11.5), if is_active { t.text_strong } else { t.text_dim });
|
||||
let w = (galley.size().x + 44.0).max(if is_active { 210.0 } else { 150.0 });
|
||||
let r = egui::Rect::from_min_size(egui::pos2(x, bar.min.y), vec2(w, 28.0));
|
||||
let resp = ui.interact(r, ui.id().with(("doctab", i)), Sense::click());
|
||||
|
|
@ -514,7 +532,7 @@ pub fn status_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
crate::canvas::go_to_page(app, n - 1);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
ui.label(egui::RichText::new("[Basic] (working) ▾").font(small.clone()));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "[Basic] (working) ▾")).font(small.clone()));
|
||||
ui.add_space(8.0);
|
||||
// Preflight: overset text is an error.
|
||||
let errors = panels::preflight_errors(app);
|
||||
|
|
@ -544,59 +562,94 @@ pub fn start_screen(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let r = ui.available_rect_before_wrap();
|
||||
ui.painter().rect_filled(r, 0.0, t.panel_darker);
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(r.shrink(48.0)), |ui| {
|
||||
ui.label(egui::RichText::new("Welcome to DesignCraft").font(semibold(28.0)).color(t.text_strong));
|
||||
ui.label(egui::RichText::new("Page layout for print and screen — fast, open and scriptable.").size(15.0).color(t.text_dim));
|
||||
ui.add_space(24.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(egui::RichText::new(" New file ").size(14.0).color(Color32::WHITE))
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new().max_rect(r.shrink(48.0)).layout(egui::Layout::top_down(if crate::i18n::is_rtl(&app.ui.language) {
|
||||
egui::Align::Max
|
||||
} else {
|
||||
egui::Align::Min
|
||||
})),
|
||||
|ui| {
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Welcome to DesignCraft")).font(semibold(28.0)).color(t.text_strong),
|
||||
));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Page layout for print and screen — fast, open and scriptable."))
|
||||
.size(15.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
ui.add_space(24.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, " New file ")).size(14.0).color(Color32::WHITE),
|
||||
))
|
||||
.fill(t.accent_strong)
|
||||
.corner_radius(16.0)
|
||||
.min_size(vec2(0.0, 32.0)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("app.newDocumentDialog", json!({}));
|
||||
}
|
||||
if ui.add(egui::Button::new(egui::RichText::new(" Open ").size(14.0)).corner_radius(16.0).min_size(vec2(0.0, 32.0))).clicked() {
|
||||
let _ = app.run("app.openDialog", json!({}));
|
||||
}
|
||||
if ui
|
||||
.add(egui::Button::new(egui::RichText::new(" Open sample magazine ").size(14.0)).corner_radius(16.0).min_size(vec2(0.0, 32.0)))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("file.newSample", json!({}));
|
||||
}
|
||||
});
|
||||
ui.add_space(20.0);
|
||||
community_card(app, ui);
|
||||
ui.add_space(24.0);
|
||||
ui.label(egui::RichText::new("Start a new document").font(semibold(15.0)).color(t.text_strong));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for p in designcraft_doc::build::PRESETS.iter().take(10) {
|
||||
let (cr, resp) = ui.allocate_exact_size(vec2(120.0, 150.0), Sense::click());
|
||||
let hov = resp.hovered();
|
||||
ui.painter().rect_filled(cr, 6.0, if hov { t.hover } else { t.panel });
|
||||
let k = (80.0 / p.width.max(p.height)) as f32;
|
||||
let pr = egui::Rect::from_center_size(cr.center() - vec2(0.0, 14.0), vec2(p.width as f32 * k, p.height as f32 * k));
|
||||
ui.painter().rect_filled(pr, 0.0, Color32::from_gray(245));
|
||||
ui.painter().text(egui::pos2(cr.center().x, cr.max.y - 26.0), egui::Align2::CENTER_CENTER, p.name, semibold(12.0), t.text_strong);
|
||||
ui.painter().text(
|
||||
egui::pos2(cr.center().x, cr.max.y - 11.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
format!("{} × {}", designcraft_geom::format_measure(p.width, p.units), designcraft_geom::format_measure(p.height, p.units)),
|
||||
egui::FontId::proportional(10.0),
|
||||
t.text_dim,
|
||||
);
|
||||
if resp.clicked() {
|
||||
let _ = app.run("file.new", json!({"preset": p.name}));
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("app.newDocumentDialog", json!({}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(crate::rtl::widget(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, " Open ")).size(14.0)))
|
||||
.corner_radius(16.0)
|
||||
.min_size(vec2(0.0, 32.0)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("app.openDialog", json!({}));
|
||||
}
|
||||
if ui
|
||||
.add(
|
||||
egui::Button::new(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, " Open sample magazine ")).size(14.0),
|
||||
))
|
||||
.corner_radius(16.0)
|
||||
.min_size(vec2(0.0, 32.0)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("file.newSample", json!({}));
|
||||
}
|
||||
});
|
||||
ui.add_space(20.0);
|
||||
community_card(app, ui);
|
||||
ui.add_space(24.0);
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Start a new document")).font(semibold(15.0)).color(t.text_strong),
|
||||
));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for p in designcraft_doc::build::PRESETS.iter().take(10) {
|
||||
let (cr, resp) = ui.allocate_exact_size(vec2(120.0, 150.0), Sense::click());
|
||||
let hov = resp.hovered();
|
||||
ui.painter().rect_filled(cr, 6.0, if hov { t.hover } else { t.panel });
|
||||
let k = (80.0 / p.width.max(p.height)) as f32;
|
||||
let pr = egui::Rect::from_center_size(cr.center() - vec2(0.0, 14.0), vec2(p.width as f32 * k, p.height as f32 * k));
|
||||
ui.painter().rect_filled(pr, 0.0, Color32::from_gray(245));
|
||||
ui.painter().text(egui::pos2(cr.center().x, cr.max.y - 26.0), egui::Align2::CENTER_CENTER, p.name, semibold(12.0), t.text_strong);
|
||||
ui.painter().text(
|
||||
egui::pos2(cr.center().x, cr.max.y - 11.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
format!("{} × {}", designcraft_geom::format_measure(p.width, p.units), designcraft_geom::format_measure(p.height, p.units)),
|
||||
egui::FontId::proportional(10.0),
|
||||
t.text_dim,
|
||||
);
|
||||
if resp.clicked() {
|
||||
let _ = app.run("file.new", json!({"preset": p.name}));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
let _ = Content::Unassigned;
|
||||
}
|
||||
|
||||
|
|
@ -610,8 +663,16 @@ fn community_card(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
crate::about::paint_mark(ui, r, crate::about::BRAND);
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("Join the ArtCraft community").font(semibold(14.0)).color(t.text_strong));
|
||||
ui.label(egui::RichText::new("Get help, share your layouts and shape what we build next.").size(12.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Join the ArtCraft community")).font(semibold(14.0)).color(t.text_strong),
|
||||
));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Get help, share your layouts and shape what we build next."))
|
||||
.size(12.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (text, id) in [("DesignCraft page", "help.appPage"), ("GitHub", "help.github"), ("getartcraft.com", "help.website")] {
|
||||
|
|
@ -628,7 +689,7 @@ fn community_card(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::about::discord_button(ui, "Join our Discord", vec2(190.0, 38.0)) {
|
||||
if crate::about::discord_button(ui, crate::i18n::tr(&app.ui.language, "Join our Discord"), vec2(190.0, 38.0)) {
|
||||
let _ = app.run("help.discord", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -61,8 +61,9 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let mut x = strip.min.x;
|
||||
for (id, label, _) in DOCK_TABS {
|
||||
let active = app.ui.dock_tab == *id;
|
||||
let g = ui.painter().layout_no_wrap(
|
||||
label.to_string(),
|
||||
let g = crate::rtl::plain(
|
||||
ui.ctx(),
|
||||
crate::i18n::tr(&app.ui.language, label),
|
||||
semibold(11.5),
|
||||
if active { Color32::from_rgb(0xf3, 0xf3, 0xf3) } else { t.text_dim },
|
||||
);
|
||||
|
|
@ -98,7 +99,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
for (id, label, icon) in ICON_PANELS {
|
||||
let open = app.ui.open_panel.as_deref() == Some(*id);
|
||||
let floats = app.ui.floating.iter().any(|(p, _)| p == id);
|
||||
if icons::button(ui, icon, 28.0, open || floats, &crate::i18n::tr(&app.ui.language, label)).clicked() {
|
||||
if icons::button(ui, icon, 28.0, open || floats, crate::i18n::tr(&app.ui.language, label)).clicked() {
|
||||
if floats {
|
||||
ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(("floating_panel", *id))));
|
||||
} else {
|
||||
|
|
@ -123,7 +124,7 @@ fn dock_header(ui: &mut egui::Ui, t: &Tokens, chevron: &str) {
|
|||
pub fn flyout(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let Some(id) = app.ui.open_panel.clone() else { return };
|
||||
let t = Tokens::get(ctx);
|
||||
let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).into_owned();
|
||||
let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).to_owned();
|
||||
let screen = ctx.content_rect();
|
||||
let dock_w = if app.ui.dock_expanded { ctx.memory(|m| m.area_rect(egui::Id::new("dock")).map(|r| r.width())).unwrap_or(280.0) } else { 0.0 };
|
||||
let pos = egui::pos2(screen.max.x - 37.0 - dock_w - 262.0, 120.0);
|
||||
|
|
@ -141,7 +142,9 @@ pub fn flyout(app: &mut DesignApp, ctx: &egui::Context) {
|
|||
ui.painter().text(close.center(), egui::Align2::CENTER_CENTER, "×", egui::FontId::proportional(15.0), t.text_dim);
|
||||
// Float: the button beside the close box, or drag the header away from the dock.
|
||||
let float_r = egui::Rect::from_min_size(egui::pos2(strip.max.x - 44.0, strip.min.y + 4.0), vec2(18.0, 18.0));
|
||||
let fr = ui.interact(float_r, ui.id().with("flyfloat"), Sense::click()).on_hover_text("Float panel");
|
||||
let fr = ui.interact(float_r, ui.id().with("flyfloat"), Sense::click()).on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Float panel"));
|
||||
});
|
||||
float_glyph(ui.painter(), float_r, t.text_dim);
|
||||
let head = egui::Rect::from_min_max(strip.min, egui::pos2(float_r.min.x - 2.0, strip.max.y));
|
||||
let hr = ui.interact(head, ui.id().with("flyhead"), Sense::drag());
|
||||
|
|
@ -218,7 +221,7 @@ pub fn dock_panel(app: &mut DesignApp, id: &str) {
|
|||
pub fn floating(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let t = Tokens::get(ctx);
|
||||
for (id, at) in app.ui.floating.clone() {
|
||||
let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).into_owned();
|
||||
let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).to_owned();
|
||||
let mut open = true;
|
||||
let mut dock = false;
|
||||
let r = egui::Window::new(label.clone())
|
||||
|
|
@ -232,14 +235,26 @@ pub fn floating(app: &mut DesignApp, ctx: &egui::Context) {
|
|||
// Header strip (drag it to move the panel): name, Dock, close.
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width().max(200.0), 24.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.panel_darker);
|
||||
ui.painter().text(strip.min + vec2(10.0, 12.0), egui::Align2::LEFT_CENTER, label, semibold(12.0), t.text_strong);
|
||||
crate::rtl::paint(ui.painter(), strip.min + vec2(10.0, 12.0), egui::Align2::LEFT_CENTER, &label, semibold(12.0), t.text_strong);
|
||||
let close = egui::Rect::from_min_size(egui::pos2(strip.max.x - 22.0, strip.min.y + 3.0), vec2(18.0, 18.0));
|
||||
if ui.interact(close, ui.id().with("fclose"), Sense::click()).on_hover_text("Close").clicked() {
|
||||
if ui
|
||||
.interact(close, ui.id().with("fclose"), Sense::click())
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Close"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
open = false;
|
||||
}
|
||||
ui.painter().text(close.center(), egui::Align2::CENTER_CENTER, "×", egui::FontId::proportional(15.0), t.text_dim);
|
||||
let dock_r = egui::Rect::from_min_size(egui::pos2(strip.max.x - 44.0, strip.min.y + 3.0), vec2(18.0, 18.0));
|
||||
if ui.interact(dock_r, ui.id().with("fdock"), Sense::click()).on_hover_text("Dock panel").clicked() {
|
||||
if ui
|
||||
.interact(dock_r, ui.id().with("fdock"), Sense::click())
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Dock panel"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
dock = true;
|
||||
}
|
||||
dock_glyph(ui.painter(), dock_r, t.text_dim);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,276 @@
|
|||
//! Original Arabic interface translations, stored in logical Unicode order.
|
||||
pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("File", "ملف"),
|
||||
("Edit", "تحرير"),
|
||||
("Layout", "تخطيط"),
|
||||
("Type", "نص"),
|
||||
("Object", "كائن"),
|
||||
("Table", "جدول"),
|
||||
("View", "عرض"),
|
||||
("Window", "نافذة"),
|
||||
("Help", "مساعدة"),
|
||||
("New", "جديد"),
|
||||
("New Document…", "مستند جديد…"),
|
||||
("New Document", "مستند جديد"),
|
||||
("Open…", "فتح…"),
|
||||
("Close", "إغلاق"),
|
||||
("Save", "حفظ"),
|
||||
("Save As…", "حفظ باسم…"),
|
||||
("Save a Copy…", "حفظ نسخة…"),
|
||||
("Revert", "استعادة"),
|
||||
("Place…", "إدراج…"),
|
||||
("Export PDF…", "تصدير PDF…"),
|
||||
("Print…", "طباعة…"),
|
||||
("Print", "طباعة"),
|
||||
("Document Setup…", "إعداد المستند…"),
|
||||
("Document Setup", "إعداد المستند"),
|
||||
("Package…", "تجميع…"),
|
||||
("Undo", "تراجع"),
|
||||
("Redo", "إعادة"),
|
||||
("Cut", "قص"),
|
||||
("Copy", "نسخ"),
|
||||
("Paste", "لصق"),
|
||||
("Clear", "مسح"),
|
||||
("Delete", "حذف"),
|
||||
("Duplicate", "تكرار"),
|
||||
("Select All", "تحديد الكل"),
|
||||
("Deselect All", "إلغاء تحديد الكل"),
|
||||
("Find/Change…", "بحث واستبدال…"),
|
||||
("Find/Change", "بحث واستبدال"),
|
||||
("Check Spelling…", "تدقيق إملائي…"),
|
||||
("Preferences…", "تفضيلات…"),
|
||||
("Preferences", "تفضيلات"),
|
||||
("Keyboard Shortcuts…", "اختصارات لوحة المفاتيح…"),
|
||||
("Keyboard Shortcuts", "اختصارات لوحة المفاتيح"),
|
||||
("Interface Language", "لغة الواجهة"),
|
||||
("Pages", "صفحات"),
|
||||
("Page", "صفحة"),
|
||||
("Margins and Columns…", "هوامش وأعمدة…"),
|
||||
("Create Guides…", "إنشاء أدلة…"),
|
||||
("Numbering & Section Options…", "خيارات الترقيم والأقسام…"),
|
||||
("Go to Page…", "انتقال إلى صفحة…"),
|
||||
("Go to Page", "انتقال إلى صفحة"),
|
||||
("Next Page", "الصفحة التالية"),
|
||||
("Previous Page", "الصفحة السابقة"),
|
||||
("First Page", "الصفحة الأولى"),
|
||||
("Last Page", "الصفحة الأخيرة"),
|
||||
("Font", "خط"),
|
||||
("Size", "حجم"),
|
||||
("Character", "حرف"),
|
||||
("Paragraph", "فقرة"),
|
||||
("Paragraph Styles", "أنماط الفقرات"),
|
||||
("Character Styles", "أنماط الحروف"),
|
||||
("Paragraph Style Options", "خيارات نمط الفقرة"),
|
||||
("Object Styles", "أنماط الكائنات"),
|
||||
("Glyphs", "محارف"),
|
||||
("Find/Replace Font…", "بحث واستبدال الخط…"),
|
||||
("Find/Replace Font", "بحث واستبدال الخط"),
|
||||
("Story Editor", "محرر النص"),
|
||||
("Edit in Story Editor", "تحرير في محرر النص"),
|
||||
("Insert Special Character", "إدراج حرف خاص"),
|
||||
("Insert White Space", "إدراج مسافة"),
|
||||
("Insert Break Character", "إدراج فاصل"),
|
||||
("Change Case", "تغيير حالة الأحرف"),
|
||||
("Text Frame Options…", "خيارات إطار النص…"),
|
||||
("Text Frame Options", "خيارات إطار النص"),
|
||||
("Footnote Options", "خيارات الحواشي"),
|
||||
("New Cross-Reference", "مرجع متقاطع جديد"),
|
||||
("Arrange", "ترتيب"),
|
||||
("Transform", "تحويل"),
|
||||
("Group", "تجميع"),
|
||||
("Ungroup", "فك التجميع"),
|
||||
("Lock", "قفل"),
|
||||
("Unlock All on Spread", "إلغاء قفل كل كائنات الحيز"),
|
||||
("Bring to Front", "إحضار إلى المقدمة"),
|
||||
("Send to Back", "إرسال إلى الخلف"),
|
||||
("Bring Forward", "إحضار إلى الأمام"),
|
||||
("Send Backward", "إرسال إلى الخلف خطوة"),
|
||||
("Fitting", "ملاءمة"),
|
||||
("Fit Frame to Content", "ملاءمة الإطار للمحتوى"),
|
||||
("Fit Content to Frame", "ملاءمة المحتوى للإطار"),
|
||||
("Fill Frame Proportionally", "ملء الإطار تناسبياً"),
|
||||
("Fit Content Proportionally", "ملاءمة المحتوى تناسبياً"),
|
||||
("Center Content", "توسيط المحتوى"),
|
||||
("Frame Fitting Options", "خيارات ملاءمة الإطار"),
|
||||
("Generate QR Code", "إنشاء رمز QR"),
|
||||
("Create Table", "إنشاء جدول"),
|
||||
("Insert Table…", "إدراج جدول…"),
|
||||
("Insert", "إدراج"),
|
||||
("Rows", "صفوف"),
|
||||
("Columns", "أعمدة"),
|
||||
("Row", "صف"),
|
||||
("Column", "عمود"),
|
||||
("Merge Cells", "دمج الخلايا"),
|
||||
("Split Cell Horizontally", "تقسيم الخلية أفقياً"),
|
||||
("Split Cell Vertically", "تقسيم الخلية عمودياً"),
|
||||
("Table Options", "خيارات الجدول"),
|
||||
("Cell Options", "خيارات الخلية"),
|
||||
("Header Rows", "صفوف الرأس"),
|
||||
("Footer Rows", "صفوف التذييل"),
|
||||
("Zoom In", "تكبير"),
|
||||
("Zoom Out", "تصغير"),
|
||||
("Fit Page in Window", "ملاءمة الصفحة للنافذة"),
|
||||
("Fit Spread in Window", "ملاءمة الحيز للنافذة"),
|
||||
("Actual Size", "الحجم الفعلي"),
|
||||
("Screen Mode", "وضع الشاشة"),
|
||||
("Normal", "عادي"),
|
||||
("Preview", "معاينة"),
|
||||
("Rulers", "مساطر"),
|
||||
("Grids & Guides", "شبكات وأدلة"),
|
||||
("Show Guides", "إظهار الأدلة"),
|
||||
("Hide Guides", "إخفاء الأدلة"),
|
||||
("Show Rulers", "إظهار المساطر"),
|
||||
("Hide Rulers", "إخفاء المساطر"),
|
||||
("Extras", "إضافات"),
|
||||
("Workspace", "مساحة العمل"),
|
||||
("New Workspace", "مساحة عمل جديدة"),
|
||||
("Properties", "خصائص"),
|
||||
("Layers", "طبقات"),
|
||||
("Swatches", "عينات الألوان"),
|
||||
("Colour", "لون"),
|
||||
("Color", "لون"),
|
||||
("Stroke", "حد"),
|
||||
("Fill", "تعبئة"),
|
||||
("Gradient", "تدرج"),
|
||||
("Transparency", "شفافية"),
|
||||
("Effects", "تأثيرات"),
|
||||
("Links", "روابط"),
|
||||
("Notes", "ملاحظات"),
|
||||
("Conditions", "شروط"),
|
||||
("Library", "مكتبة"),
|
||||
("Text Wrap", "التفاف النص"),
|
||||
("Align", "محاذاة"),
|
||||
("Pathfinder", "دمج المسارات"),
|
||||
("Transform Again", "تكرار التحويل"),
|
||||
("Interactive", "تفاعلي"),
|
||||
("Buttons and Forms", "أزرار ونماذج"),
|
||||
("Hyperlinks", "ارتباطات تشعبية"),
|
||||
("Bookmarks", "إشارات مرجعية"),
|
||||
("Show All Menu Items", "إظهار كل عناصر القائمة"),
|
||||
("About DesignCraft", "حول DesignCraft"),
|
||||
("Welcome to DesignCraft", "مرحباً بك في DesignCraft"),
|
||||
("Page layout for print and screen — fast, open and scriptable.", "تخطيط الصفحات للطباعة والشاشة — سريع ومفتوح وقابل للبرمجة."),
|
||||
(" New file ", " ملف جديد "),
|
||||
(" Open ", " فتح "),
|
||||
(" Open sample magazine ", " فتح نموذج المجلة "),
|
||||
("Start a new document", "بدء مستند جديد"),
|
||||
("No document open", "لا يوجد مستند مفتوح"),
|
||||
("Join the ArtCraft community", "انضم إلى مجتمع ArtCraft"),
|
||||
("Get help, share your layouts and shape what we build next.", "احصل على المساعدة وشارك تصميماتك وساهم في تطوير التطبيق."),
|
||||
("Join our Discord", "انضم إلى Discord"),
|
||||
("Community", "المجتمع"),
|
||||
("Join the community", "انضم إلى المجتمع"),
|
||||
("discord.gg/artcraft — help, feedback and show-and-tell", "discord.gg/artcraft — مساعدة وآراء وعرض الأعمال"),
|
||||
(" OK ", " موافق "),
|
||||
("Float panel", "تعويم اللوحة"),
|
||||
("Dock panel", "إرساء اللوحة"),
|
||||
("OK", "موافق"),
|
||||
("Cancel", "إلغاء"),
|
||||
("Apply", "تطبيق"),
|
||||
("Done", "تم"),
|
||||
("Reset", "إعادة ضبط"),
|
||||
("Add", "إضافة"),
|
||||
("Remove", "إزالة"),
|
||||
("Name", "اسم"),
|
||||
("Width", "عرض"),
|
||||
("Height", "ارتفاع"),
|
||||
("Portrait", "عمودي"),
|
||||
("Landscape", "أفقي"),
|
||||
("Orientation", "اتجاه الصفحة"),
|
||||
("Units", "وحدات"),
|
||||
("Preset", "إعداد مسبق"),
|
||||
("Facing Pages", "صفحات متقابلة"),
|
||||
("Primary Text Frame", "إطار النص الرئيسي"),
|
||||
("Number of Pages", "عدد الصفحات"),
|
||||
("Start Page #", "رقم صفحة البداية"),
|
||||
("Page Size", "حجم الصفحة"),
|
||||
("Margins", "هوامش"),
|
||||
("Bleed", "نزف"),
|
||||
("Slug", "مساحة إضافية"),
|
||||
("Top", "أعلى"),
|
||||
("Bottom", "أسفل"),
|
||||
("Left", "يسار"),
|
||||
("Right", "يمين"),
|
||||
("Inside", "داخلي"),
|
||||
("Outside", "خارجي"),
|
||||
("Gutter", "فاصل الأعمدة"),
|
||||
("Binding", "اتجاه التجليد"),
|
||||
("Left to Right", "من اليسار إلى اليمين"),
|
||||
("Right to Left", "من اليمين إلى اليسار"),
|
||||
("No Selection", "لا يوجد تحديد"),
|
||||
("Rectangle", "مستطيل"),
|
||||
("Ellipse", "شكل بيضاوي"),
|
||||
("Polygon", "مضلع"),
|
||||
("Polygon Settings", "إعدادات المضلع"),
|
||||
("Text", "نص"),
|
||||
("Image", "صورة"),
|
||||
("Line", "خط مستقيم"),
|
||||
("Position", "موضع"),
|
||||
("Appearance", "مظهر"),
|
||||
("Opacity", "عتامة"),
|
||||
("Corner", "زاوية"),
|
||||
("Rotation", "دوران"),
|
||||
("Rulers & Grids", "مساطر وشبكات"),
|
||||
("Guides", "أدلة"),
|
||||
("Flex Layout", "تخطيط مرن"),
|
||||
("Auto Size", "حجم تلقائي"),
|
||||
("Tracking", "تباعد الحروف"),
|
||||
("Leading", "تباعد الأسطر"),
|
||||
("Kerning", "تقارب الحروف"),
|
||||
("Language", "لغة"),
|
||||
("Hyphenation", "وصل الكلمات"),
|
||||
("Alignment", "محاذاة"),
|
||||
("Indent", "مسافة بادئة"),
|
||||
("Space Before", "مسافة قبل"),
|
||||
("Space After", "مسافة بعد"),
|
||||
("New Layer", "طبقة جديدة"),
|
||||
("New Swatch", "عينة لون جديدة"),
|
||||
("Object Layer Options", "خيارات طبقات الكائن"),
|
||||
("Colour Picker", "منتقي الألوان"),
|
||||
("Color Picker", "منتقي الألوان"),
|
||||
("Color Settings", "إعدادات الألوان"),
|
||||
("User Dictionary", "قاموس المستخدم"),
|
||||
("Menu Customization", "تخصيص القوائم"),
|
||||
("Import Options", "خيارات الاستيراد"),
|
||||
("Place PDF", "إدراج PDF"),
|
||||
("Find", "بحث"),
|
||||
("Change", "استبدال"),
|
||||
("Find What", "نص البحث"),
|
||||
("Change To", "استبدال بـ"),
|
||||
("Find Next", "بحث عن التالي"),
|
||||
("Change All", "استبدال الكل"),
|
||||
("Search", "بحث"),
|
||||
("Document", "مستند"),
|
||||
("Selection", "تحديد"),
|
||||
("All Documents", "كل المستندات"),
|
||||
("Direction", "اتجاه"),
|
||||
("Horizontal", "أفقي"),
|
||||
("Vertical", "عمودي"),
|
||||
("On", "تشغيل"),
|
||||
("Off", "إيقاف"),
|
||||
("None", "بدون"),
|
||||
("[None]", "[بدون]"),
|
||||
("Place with Import Options…", "إدراج مع خيارات الاستيراد…"),
|
||||
("Print Booklet…", "طباعة كتيب…"),
|
||||
("Export", "تصدير"),
|
||||
("Export Page as PNG…", "تصدير الصفحة بصيغة PNG…"),
|
||||
("Export IDML…", "تصدير IDML…"),
|
||||
("Export EPUB…", "تصدير EPUB…"),
|
||||
("Export EPUB (Fixed Layout)…", "تصدير EPUB بتخطيط ثابت…"),
|
||||
("Export PDF (Interactive)…", "تصدير PDF تفاعلي…"),
|
||||
("Export HTML…", "تصدير HTML…"),
|
||||
("Export XML…", "تصدير XML…"),
|
||||
("Export Text…", "تصدير النص…"),
|
||||
("Quick Actions", "إجراءات سريعة"),
|
||||
("Import File", "استيراد ملف"),
|
||||
("Insert Page", "إدراج صفحة"),
|
||||
("Edit Page", "تحرير الصفحة"),
|
||||
("Adjust Layout", "ضبط التخطيط"),
|
||||
("Smart Guides", "أدلة ذكية"),
|
||||
("Lock Guides", "قفل الأدلة"),
|
||||
("Show Document Grid", "إظهار شبكة المستند"),
|
||||
("Show Baseline Grid", "إظهار شبكة خطوط الأساس"),
|
||||
("Options", "خيارات"),
|
||||
("More Options", "خيارات إضافية"),
|
||||
("Dialog", "مربع حوار"),
|
||||
];
|
||||
|
|
@ -783,5 +783,11 @@ pub fn button(ui: &mut egui::Ui, name: &str, size: f32, selected: bool, tip: &st
|
|||
}
|
||||
let pad = size * 0.16;
|
||||
paint(ui.painter(), r.shrink(pad), name, if selected { t.text_strong } else { t.icon });
|
||||
if tip.is_empty() { resp } else { resp.on_hover_text(tip) }
|
||||
if tip.is_empty() {
|
||||
resp
|
||||
} else {
|
||||
resp.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, tip);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub mod icons;
|
|||
pub mod menus;
|
||||
pub mod panels;
|
||||
pub mod render_worker;
|
||||
mod rtl;
|
||||
pub mod story_editor;
|
||||
pub mod taskbar;
|
||||
pub mod theme;
|
||||
|
|
@ -132,7 +133,7 @@ pub struct UiState {
|
|||
pub dynamic_spelling: bool,
|
||||
/// Preferences › Story Editor Display: text size (points).
|
||||
pub story_editor_size: f32,
|
||||
/// Edit › Interface Language: "" (English), "de", "fr", "es" or "ja".
|
||||
/// Edit › Interface Language: supported codes are listed in `i18n::LANGUAGES`.
|
||||
pub language: String,
|
||||
/// Edit › Transparency Flattener Presets: "" (none), "high", "medium" or "low" for PDF export.
|
||||
pub flattener: String,
|
||||
|
|
@ -400,6 +401,8 @@ pub struct DesignApp {
|
|||
queued_shots: Vec<(u64, f64, u32)>,
|
||||
shot_token: u64,
|
||||
styled: bool,
|
||||
/// The interface language the UI fonts were installed for (it orders the CJK fallbacks).
|
||||
fonts_lang: String,
|
||||
pub restyle: bool,
|
||||
fonts_ready: bool,
|
||||
pub integrated_titlebar: bool,
|
||||
|
|
@ -438,6 +441,7 @@ impl DesignApp {
|
|||
queued_shots: vec![],
|
||||
shot_token: 0,
|
||||
styled: false,
|
||||
fonts_lang: String::new(),
|
||||
restyle: false,
|
||||
fonts_ready: false,
|
||||
integrated_titlebar: false,
|
||||
|
|
@ -582,10 +586,15 @@ impl DesignApp {
|
|||
ctx.set_zoom_factor(scale);
|
||||
}
|
||||
if !self.styled {
|
||||
theme::install_fonts(ctx);
|
||||
theme::install_fonts(ctx, &self.ui.language);
|
||||
self.fonts_lang = self.ui.language.clone();
|
||||
self.styled = true;
|
||||
self.restyle = true;
|
||||
} else {
|
||||
if self.fonts_lang != self.ui.language {
|
||||
theme::install_fonts(ctx, &self.ui.language);
|
||||
self.fonts_lang = self.ui.language.clone();
|
||||
}
|
||||
self.fonts_ready = true;
|
||||
}
|
||||
if self.restyle {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
|||
("app.menus", "Menus…", None, "{} — show or hide menu items"),
|
||||
("window.hideMenuItem", "Hide Menu Item", None, "{item: \"Menu/Label\", hidden?: bool}"),
|
||||
("edit.dynamicSpelling", "Dynamic Spelling", None, "{on?: bool} — underline misspelled words on the canvas"),
|
||||
("app.language", "Interface Language", None, "{lang: \"\"|de|fr|es|ja} — menus and panel names (the macOS menu bar follows on the next launch)"),
|
||||
(
|
||||
"app.language",
|
||||
"Interface Language",
|
||||
None,
|
||||
"{lang: \"\"|de|fr|es|ja|zh|ar} — menus and panel names (the macOS menu bar follows on the next launch)",
|
||||
),
|
||||
(
|
||||
"app.flattener",
|
||||
"Transparency Flattener Presets",
|
||||
|
|
@ -221,6 +226,8 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"ui:app.language|Français|{\"lang\": \"fr\"}",
|
||||
"ui:app.language|Español|{\"lang\": \"es\"}",
|
||||
"ui:app.language|日本語|{\"lang\": \"ja\"}",
|
||||
"ui:app.language|简体中文|{\"lang\": \"zh\"}",
|
||||
"ui:app.language|العربية|{\"lang\": \"ar\"}",
|
||||
"<",
|
||||
">Transparency Flattener Presets",
|
||||
"ui:app.flattener|None (keep transparency)|{\"preset\": \"\"}",
|
||||
|
|
@ -1717,16 +1724,25 @@ pub fn activate(app: &mut DesignApp, id: &str, params: &Value) {
|
|||
/// The menu bar contents (inside the app bar; macOS uses the native menu instead).
|
||||
pub fn menu_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let lang = app.ui.language.clone();
|
||||
for (menu, entries) in menu_tree() {
|
||||
ui.menu_button(crate::i18n::tr(&lang, menu), |ui| {
|
||||
ui.set_min_width(240.0);
|
||||
let hidden = menu_items(app, ui, &entries, menu);
|
||||
if hidden > 0 && !app.ui.show_full_menus {
|
||||
ui.separator();
|
||||
if ui.button(crate::i18n::tr(&lang, "Show All Menu Items")).clicked() {
|
||||
app.ui.show_full_menus = true;
|
||||
}
|
||||
let mut menus = menu_tree();
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
menus.reverse();
|
||||
}
|
||||
for (menu, entries) in menus {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&lang, menu)), |ui| {
|
||||
if crate::i18n::is_rtl(&lang) {
|
||||
ui.set_max_width(320.0);
|
||||
}
|
||||
ui.with_layout(egui::Layout::top_down(if crate::i18n::is_rtl(&lang) { egui::Align::Max } else { egui::Align::Min }), |ui| {
|
||||
ui.set_min_width(240.0);
|
||||
let hidden = menu_items(app, ui, &entries, menu);
|
||||
if hidden > 0 && !app.ui.show_full_menus {
|
||||
ui.separator();
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&lang, "Show All Menu Items"))).clicked() {
|
||||
app.ui.show_full_menus = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1746,8 +1762,8 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str
|
|||
}
|
||||
Item::Sub(name, children) => {
|
||||
let sub = format!("{path}/{name}");
|
||||
let shown = crate::i18n::tr(&app.ui.language, name).into_owned();
|
||||
ui.menu_button(shown, |ui| {
|
||||
let shown = crate::i18n::tr(&app.ui.language, name).to_owned();
|
||||
ui.menu_button(crate::rtl::widget(ui, shown), |ui| {
|
||||
hidden += menu_items(app, ui, children, &sub);
|
||||
});
|
||||
}
|
||||
|
|
@ -1757,9 +1773,9 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str
|
|||
let text = match checked(app, id, params) {
|
||||
Some(true) => format!("✓ {label}"),
|
||||
Some(false) => format!(" {label}"),
|
||||
None => label.into_owned(),
|
||||
None => label.to_owned(),
|
||||
};
|
||||
let mut b = egui::Button::new(text);
|
||||
let mut b = egui::Button::new(crate::rtl::widget(ui, text));
|
||||
if let Some(sc) = shortcut_of(app, id) {
|
||||
b = b.shortcut_text(shortcut_text(&sc));
|
||||
}
|
||||
|
|
@ -2058,12 +2074,59 @@ mod tests {
|
|||
assert_eq!(checked(&app, "app.language", &json!({"lang": "de"})), Some(true));
|
||||
assert_eq!(crate::i18n::tr(&app.ui.language, "Window"), "Fenster");
|
||||
assert!(run_ui(&mut app, "app.language", &json!({"lang": "xx"})).unwrap().is_err());
|
||||
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "zh"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.language, "zh");
|
||||
assert_eq!(crate::i18n::tr(&app.ui.language, "File"), "文件");
|
||||
|
||||
app.run("file.new", json!({"pages": 4, "facingPages": true})).unwrap();
|
||||
let before = serde_json::to_value(&app.session.doc().unwrap().doc).unwrap();
|
||||
run_ui(&mut app, "app.language", &json!({"lang": "ar"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.language, "ar");
|
||||
assert_eq!(checked(&app, "app.language", &json!({"lang": "ar"})), Some(true));
|
||||
assert_eq!(before, serde_json::to_value(&app.session.doc().unwrap().doc).unwrap());
|
||||
for (title, _) in menu_tree() {
|
||||
assert_ne!(crate::i18n::tr("ar", title), title, "{title}");
|
||||
}
|
||||
// Every menu title has a translation (Japanese never matches the English).
|
||||
for (title, _) in menu_tree() {
|
||||
assert_ne!(crate::i18n::tr("ja", title), title, "{title}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arabic_dialog_and_language_switch_keep_finite_widget_geometry() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut crate::DesignApp| {
|
||||
for _ in 0..4 {
|
||||
let mut output = ctx.run_ui(
|
||||
egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1440.0, 900.0))),
|
||||
max_texture_side: Some(8192),
|
||||
..Default::default()
|
||||
},
|
||||
|ui| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
);
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
};
|
||||
app.run("app.language", json!({"lang": "ar"})).unwrap();
|
||||
frame(&mut app);
|
||||
app.run("app.newDocumentDialog", json!({})).unwrap();
|
||||
frame(&mut app); // egui asserts that all allocated rectangles are finite.
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
for lang in ["zh", "", "ar"] {
|
||||
app.run("app.language", json!({"lang": lang})).unwrap();
|
||||
frame(&mut app);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_setup_and_colors() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
.and_then(|a| a["chars"]["conditions"].as_array().map(|v| v.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()))
|
||||
.unwrap_or_default();
|
||||
if conditions.is_empty() {
|
||||
ui.label(egui::RichText::new("No conditions. Create one, then select text and click it to apply.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "No conditions. Create one, then select text and click it to apply."))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
}
|
||||
for c in &conditions {
|
||||
let (row, _) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::hover());
|
||||
|
|
@ -58,7 +63,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
nr.context_menu(|ui| {
|
||||
if ui.button("Delete Condition").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Condition"))).clicked() {
|
||||
let _ = app.run("condition.delete", json!({"name": c.name}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -69,7 +74,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let mut name: String = ui.data(|d| d.get_temp(id)).unwrap_or_default();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut name).hint_text("New condition name").desired_width(150.0));
|
||||
if ui.button("New").clicked() && !name.trim().is_empty() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() && !name.trim().is_empty() {
|
||||
match app.run("condition.new", json!({"name": name.trim()})) {
|
||||
Ok(_) => name.clear(),
|
||||
Err(e) => app.status(format!("Conditional Text: {e}")),
|
||||
|
|
@ -77,7 +82,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
});
|
||||
ui.data_mut(|d| d.insert_temp(id, name));
|
||||
if has_text && !applied.is_empty() && ui.button("Remove Conditions from Selection").clicked() {
|
||||
if has_text
|
||||
&& !applied.is_empty()
|
||||
&& ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Remove Conditions from Selection"))).clicked()
|
||||
{
|
||||
let _ = app.run("condition.apply", json!({"name": null, "only": true}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let mut insert: Option<char> = None;
|
||||
if !st.recent.is_empty() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(egui::RichText::new("Recently Used").size(10.5).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Recently Used")).size(10.5).color(t.text_dim));
|
||||
for c in st.recent.clone() {
|
||||
if ui.small_button(c.to_string()).on_hover_text(format!("U+{:04X}", c as u32)).clicked() {
|
||||
insert = Some(c);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ pub fn hyperlinks(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.separator();
|
||||
let list = app.session.execute("hyperlink.list", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default();
|
||||
if list.is_empty() {
|
||||
ui.label(egui::RichText::new("No hyperlinks. Select text or a frame, type a URL and click New.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "No hyperlinks. Select text or a frame, type a URL and click New."))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
}
|
||||
for h in list {
|
||||
let hid = h["id"].clone();
|
||||
|
|
@ -56,14 +61,17 @@ pub fn hyperlinks(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
|
||||
pub fn bookmarks(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
if ui.button("New Bookmark for This Page").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Bookmark for This Page"))).clicked() {
|
||||
let page = crate::canvas::current_page(app).unwrap_or(0) + 1;
|
||||
let _ = app.run("bookmark.add", json!({"page": page}));
|
||||
}
|
||||
ui.separator();
|
||||
let list = app.session.execute("bookmark.list", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default();
|
||||
if list.is_empty() {
|
||||
ui.label(egui::RichText::new("No bookmarks. They become the PDF outline.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "No bookmarks. They become the PDF outline.")).size(11.0).color(t.text_dim),
|
||||
));
|
||||
}
|
||||
for (i, b) in list.iter().enumerate() {
|
||||
let page = b["page"].as_u64().unwrap_or(0) as usize;
|
||||
|
|
@ -91,7 +99,10 @@ pub fn articles(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.separator();
|
||||
let list = app.session.execute("article.list", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default();
|
||||
if list.is_empty() {
|
||||
ui.label(egui::RichText::new("No articles: exports follow page order.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "No articles: exports follow page order.")).size(11.0).color(t.text_dim),
|
||||
));
|
||||
}
|
||||
for a in list {
|
||||
let name = a["name"].as_str().unwrap_or("").to_string();
|
||||
|
|
@ -126,7 +137,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let mut name: String = ui.data(|d| d.get_temp(id)).unwrap_or_default();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut name).hint_text("New tag").desired_width(140.0));
|
||||
if ui.button("New").clicked() && !name.trim().is_empty() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() && !name.trim().is_empty() {
|
||||
match app.run("xml.newTag", json!({"name": name.trim()})) {
|
||||
Ok(_) => name.clear(),
|
||||
Err(e) => app.status(format!("Tags: {e}")),
|
||||
|
|
@ -156,7 +167,7 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let _ = if text_sel { app.run("xml.tagText", json!({"tag": null})) } else { app.run("xml.tag", json!({"tag": null})) };
|
||||
}
|
||||
let mut markers = app.ui.tag_markers;
|
||||
if ui.checkbox(&mut markers, "Show Tag Markers").changed() {
|
||||
if ui.checkbox(&mut markers, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Show Tag Markers"))).changed() {
|
||||
let _ = app.run("view.tagMarkers", json!({"on": markers}));
|
||||
}
|
||||
ui.separator();
|
||||
|
|
@ -221,10 +232,10 @@ pub fn tags(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Export XML…").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Export XML…"))).clicked() {
|
||||
let _ = app.run("app.exportXml", json!({}));
|
||||
}
|
||||
if ui.button("Import XML…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Import XML…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("xml"))
|
||||
&& let Err(e) = app.run("file.importXml", json!({"path": path}))
|
||||
{
|
||||
|
|
@ -240,7 +251,12 @@ pub fn states(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let info = app.session.execute("states.list", &json!({})).ok();
|
||||
let list = info.as_ref().and_then(|i| i["states"].as_array().cloned()).unwrap_or_default();
|
||||
if list.is_empty() {
|
||||
ui.label(egui::RichText::new("Select two or more objects (or a group) to make a multi-state object.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select two or more objects (or a group) to make a multi-state object."))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
if ui.add_enabled(n > 0, egui::Button::new("Convert Selection to Multi-State Object")).clicked()
|
||||
&& let Err(e) = app.run("states.create", json!({}))
|
||||
{
|
||||
|
|
@ -264,7 +280,10 @@ pub fn states(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(id) = app.session.active().and_then(|d| d.selection.items.first().copied()) else {
|
||||
ui.label(egui::RichText::new("Select an object to make it a button.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select an object to make it a button.")).size(11.0).color(t.text_dim),
|
||||
));
|
||||
return;
|
||||
};
|
||||
let cur = app.session.active().and_then(|d| d.doc.item(id)).and_then(|it| it.button.clone());
|
||||
|
|
@ -289,7 +308,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
];
|
||||
let mut pick = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("On Release");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "On Release"));
|
||||
egui::ComboBox::from_id_salt("button_action").selected_text(labels.iter().find(|l| l.0 == kind).map_or("", |l| l.1)).show_ui(ui, |ui| {
|
||||
for (k, l) in labels {
|
||||
if ui.selectable_label(k == kind, l).clicked() {
|
||||
|
|
@ -306,7 +325,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let total = app.session.active().map_or(1, |d| d.doc.page_count());
|
||||
let mut n = page + 1;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Page");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Page"));
|
||||
if ui.add(egui::DragValue::new(&mut n).range(1..=total)).changed() {
|
||||
params = Some(json!({"action": "page", "page": n - 1}));
|
||||
}
|
||||
|
|
@ -326,7 +345,10 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
{
|
||||
app.status(format!("Buttons: {e}"));
|
||||
}
|
||||
ui.label(egui::RichText::new("Buttons act in interactive PDF export.").size(10.5).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Buttons act in interactive PDF export.")).size(10.5).color(t.text_dim),
|
||||
));
|
||||
// Form field.
|
||||
ui.separator();
|
||||
let ff = app.session.active().and_then(|d| d.doc.item(id)).and_then(|it| it.form_field.clone());
|
||||
|
|
@ -341,7 +363,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
];
|
||||
let cur = ff.as_ref().map(|f| f.kind);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Form Field");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Form Field"));
|
||||
egui::ComboBox::from_id_salt("form_kind").selected_text(kinds.iter().find(|k| k.0 == cur).map_or("", |k| k.1)).show_ui(ui, |ui| {
|
||||
for (k, l) in kinds {
|
||||
if ui.selectable_label(k == cur, l).clicked() {
|
||||
|
|
@ -359,33 +381,33 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.data(|d| d.get_temp(key)).unwrap_or((f.name.clone(), f.value.clone(), f.options.join(", ")));
|
||||
let mut commit = false;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Name"));
|
||||
commit |= ui.text_edit_singleline(&mut name).lost_focus();
|
||||
});
|
||||
if f.kind == K::CheckBox {
|
||||
let mut on = !f.value.is_empty() && f.value != "Off";
|
||||
if ui.checkbox(&mut on, "Checked by default").changed() {
|
||||
if ui.checkbox(&mut on, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Checked by default"))).changed() {
|
||||
let _ = app.run("form.set", json!({"kind": f.kind, "value": if on { "On" } else { "" }}));
|
||||
}
|
||||
} else if f.kind != K::Signature {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Default");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Default"));
|
||||
commit |= ui.text_edit_singleline(&mut value).lost_focus();
|
||||
});
|
||||
}
|
||||
if matches!(f.kind, K::ComboBox | K::ListBox) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Choices");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Choices"));
|
||||
commit |= ui.text_edit_singleline(&mut opts).on_hover_text("Separated by commas").lost_focus();
|
||||
});
|
||||
}
|
||||
let mut req = f.required;
|
||||
if ui.checkbox(&mut req, "Required").changed() {
|
||||
if ui.checkbox(&mut req, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Required"))).changed() {
|
||||
let _ = app.run("form.set", json!({"kind": f.kind, "required": req}));
|
||||
}
|
||||
if f.kind == K::TextField {
|
||||
let mut ml = f.multiline;
|
||||
if ui.checkbox(&mut ml, "Multiline").changed() {
|
||||
if ui.checkbox(&mut ml, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Multiline"))).changed() {
|
||||
let _ = app.run("form.set", json!({"kind": f.kind, "multiline": ml}));
|
||||
}
|
||||
}
|
||||
|
|
@ -404,7 +426,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(page) = crate::canvas::current_page(app) else {
|
||||
ui.label(egui::RichText::new("No document.").size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No document.")).size(11.0).color(t.text_dim));
|
||||
return;
|
||||
};
|
||||
let Some(d) = app.session.active().map(|d| d.doc.clone()) else { return };
|
||||
|
|
@ -442,7 +464,7 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
if !pg.guides.is_empty() {
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Liquid guides").size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Liquid guides")).size(11.0).color(t.text_dim));
|
||||
for (gi, g) in pg.guides.iter().enumerate() {
|
||||
let mut on = g.liquid;
|
||||
let label = format!("{:?} at {:.1}", g.orientation, g.position);
|
||||
|
|
@ -454,14 +476,14 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.separator();
|
||||
let key = egui::Id::new("alt_layout");
|
||||
let (mut name, mut w, mut h): (String, f64, f64) = ui.data(|x| x.get_temp(key)).unwrap_or(("Tablet H".into(), 1024.0, 768.0));
|
||||
ui.label(egui::RichText::new("Create Alternate Layout").size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Create Alternate Layout")).size(11.0).color(t.text_dim));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut name).desired_width(90.0));
|
||||
ui.add(egui::DragValue::new(&mut w).range(1.0..=15552.0).suffix(" pt"));
|
||||
ui.label("×");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "×"));
|
||||
ui.add(egui::DragValue::new(&mut h).range(1.0..=15552.0).suffix(" pt"));
|
||||
});
|
||||
if ui.button("Create").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Create"))).clicked()
|
||||
&& let Err(e) = app.run("layout.createAlternate", json!({"name": name, "width": w, "height": h}))
|
||||
{
|
||||
app.status(format!("Alternate layout: {e}"));
|
||||
|
|
@ -473,7 +495,12 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
pub fn media(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Ok(m) = app.session.execute("media.get", &json!({})) else {
|
||||
ui.label(egui::RichText::new("Select a placed video or sound (File › Place a .mp4, .mov, .mp3, .wav …).").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select a placed video or sound (File › Place a .mp4, .mov, .mp3, .wav …)."))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
return;
|
||||
};
|
||||
ui.label(egui::RichText::new(format!("{} — {}", m["kind"].as_str().unwrap_or(""), m["name"].as_str().unwrap_or(""))).strong());
|
||||
|
|
@ -484,8 +511,8 @@ pub fn media(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Poster");
|
||||
if ui.button("Choose Image…").clicked()
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Poster"));
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Choose Image…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("place"))
|
||||
&& let Err(e) = app.run("media.options", json!({"poster": {"path": path}}))
|
||||
{
|
||||
|
|
@ -495,7 +522,10 @@ pub fn media(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let _ = app.run("media.options", json!({"poster": null}));
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new("Plays in EPUB export; print and PDF show the poster.").size(10.5).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Plays in EPUB export; print and PDF show the poster.")).size(10.5).color(t.text_dim),
|
||||
));
|
||||
}
|
||||
|
||||
/// Page Transitions panel: the current spread's transition for interactive PDF.
|
||||
|
|
@ -509,7 +539,7 @@ pub fn transitions(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.label(egui::RichText::new(format!("Spread {}", si + 1)).strong());
|
||||
let mut kind = cur.map(|c| c.kind);
|
||||
egui::ComboBox::from_id_salt("transition_kind").selected_text(kind.map_or("None", K::label)).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut kind, None, "None");
|
||||
ui.selectable_value(&mut kind, None, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "None")));
|
||||
for k in K::ALL {
|
||||
ui.selectable_value(&mut kind, Some(k), k.label());
|
||||
}
|
||||
|
|
@ -519,7 +549,7 @@ pub fn transitions(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let mut changed = kind != cur.map(|c| c.kind);
|
||||
if kind.is_some() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Speed");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Speed"));
|
||||
changed |= ui.add(egui::DragValue::new(&mut duration).range(0.1..=10.0).speed(0.1).suffix(" s")).changed();
|
||||
});
|
||||
if matches!(kind, Some(K::Blinds | K::Box | K::Split)) {
|
||||
|
|
@ -537,10 +567,15 @@ pub fn transitions(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
if changed {
|
||||
let _ = app.run("page.transition", params(false));
|
||||
}
|
||||
if ui.button("Apply to All Spreads").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply to All Spreads"))).clicked() {
|
||||
let _ = app.run("page.transition", params(true));
|
||||
}
|
||||
ui.label(egui::RichText::new("Transitions play in interactive PDF (full-screen mode).").size(10.5).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Transitions play in interactive PDF (full-screen mode)."))
|
||||
.size(10.5)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
}
|
||||
|
||||
/// Track Changes panel: tracking on/off and each change with Accept / Reject.
|
||||
|
|
@ -548,13 +583,13 @@ pub fn track_changes(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let Some(on) = app.session.active().map(|d| d.doc.settings.track_changes) else { return };
|
||||
let mut track = on;
|
||||
if ui.checkbox(&mut track, "Track Changes in All Stories").changed() {
|
||||
if ui.checkbox(&mut track, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Track Changes in All Stories"))).changed() {
|
||||
let _ = app.run("changes.track", json!({"on": track}));
|
||||
}
|
||||
let list = app.session.execute("changes.list", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default();
|
||||
ui.separator();
|
||||
if list.is_empty() {
|
||||
ui.label(egui::RichText::new("No changes.").size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No changes.")).size(11.0).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
|
||||
|
|
@ -583,10 +618,10 @@ pub fn track_changes(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Accept All").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Accept All"))).clicked() {
|
||||
let _ = app.run("changes.acceptAll", json!({}));
|
||||
}
|
||||
if ui.button("Reject All").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Reject All"))).clicked() {
|
||||
let _ = app.run("changes.rejectAll", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -141,18 +141,18 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
|
|||
let _ = app.run("layer.activate", json!({"id": l.id.0}));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Delete Layer").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Layer"))).clicked() {
|
||||
let _ = app.run("layer.delete", json!({"id": l.id.0}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Move Selection Here").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move Selection Here"))).clicked() {
|
||||
let _ = app.run("object.setLayer", json!({"layer": l.id.0}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
let active = app.session.active().map(|d| d.active_layer);
|
||||
if let Some(a) = active.filter(|a| *a != l.id)
|
||||
&& ui.button("Merge into Active Layer").clicked()
|
||||
&& ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Merge into Active Layer"))).clicked()
|
||||
{
|
||||
let _ = app.run("layer.merge", json!({"ids": [a.0, l.id.0], "into": a.0}));
|
||||
ui.close();
|
||||
|
|
@ -168,7 +168,7 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
|
|||
ui.close();
|
||||
}
|
||||
}
|
||||
if ui.button("Delete Unused Layers").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Unused Layers"))).clicked() {
|
||||
let _ = app.run("layer.deleteUnused", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let conveyor = app.session.conveyor.iter().map(|c| c.0.clone()).collect::<Vec<_>>();
|
||||
ui.label(egui::RichText::new(format!("Conveyor ({})", conveyor.len())).strong());
|
||||
if conveyor.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new("Collect objects with the Content Collector (B); place them with the Content Placer.").size(10.5).color(t.text_dim),
|
||||
);
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(
|
||||
&app.ui.language,
|
||||
"Collect objects with the Content Collector (B); place them with the Content Placer.",
|
||||
))
|
||||
.size(10.5)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
} else {
|
||||
ui.label(egui::RichText::new(conveyor.join(", ")).size(10.5));
|
||||
if ui.small_button("Clear").clicked() {
|
||||
|
|
@ -23,11 +29,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("New Library…").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Library…"))).clicked() {
|
||||
let path = app.services.pick_save.as_mut().and_then(|f| f("Library.dclib"));
|
||||
let _ = app.run("library.new", json!({"path": path}));
|
||||
}
|
||||
if ui.button("Open…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Open…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("library"))
|
||||
&& let Err(e) = app.run("library.open", json!({"path": path}))
|
||||
{
|
||||
|
|
@ -35,7 +41,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
});
|
||||
let Ok(items) = app.session.execute("library.list", &json!({})) else {
|
||||
ui.label(egui::RichText::new("No library open.").size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No library open.")).size(11.0).color(t.text_dim));
|
||||
return;
|
||||
};
|
||||
let has_sel = app.session.active().is_some_and(|d| !d.selection.items.is_empty());
|
||||
|
|
@ -47,7 +53,12 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.separator();
|
||||
let items = items.as_array().cloned().unwrap_or_default();
|
||||
if items.is_empty() {
|
||||
ui.label(egui::RichText::new("The library is empty. Select objects and Add Selection.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "The library is empty. Select objects and Add Selection."))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
));
|
||||
}
|
||||
for it in items {
|
||||
let i = it["index"].clone();
|
||||
|
|
@ -72,13 +83,13 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("New Book…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Book…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_save.as_mut().and_then(|f| f("Book.dcbook"))
|
||||
&& let Err(e) = app.run("book.new", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Book: {e}"));
|
||||
}
|
||||
if ui.button("Open…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Open…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("book"))
|
||||
&& let Err(e) = app.run("book.open", json!({"path": path}))
|
||||
{
|
||||
|
|
@ -86,7 +97,7 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
});
|
||||
let Ok(info) = app.session.execute("book.list", &json!({})) else {
|
||||
ui.label(egui::RichText::new("No book open.").size(11.0).color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No book open.")).size(11.0).color(t.text_dim));
|
||||
return;
|
||||
};
|
||||
let source = info["styleSource"].as_u64().unwrap_or(0) as usize;
|
||||
|
|
@ -112,19 +123,19 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
ui.separator();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if ui.button("Add Document…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Add Document…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("open"))
|
||||
&& let Err(e) = app.run("book.add", json!({"path": path}))
|
||||
{
|
||||
app.status(format!("Book: {e}"));
|
||||
}
|
||||
if ui.button("Update Numbering").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Update Numbering"))).clicked() {
|
||||
let _ = app.run("book.paginate", json!({}));
|
||||
}
|
||||
if ui.button("Synchronize").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Synchronize"))).clicked() {
|
||||
let _ = app.run("book.syncStyles", json!({}));
|
||||
}
|
||||
if ui.button("Export PDF…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Export PDF…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_save.as_mut().and_then(|f| f("Book.pdf"))
|
||||
&& let Err(e) = app.run("book.exportPdf", json!({"path": path}))
|
||||
{
|
||||
|
|
@ -139,7 +150,10 @@ pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let sel_id = egui::Id::new("scripts_sel");
|
||||
let mut sel: usize = ui.data(|d| d.get_temp(sel_id)).unwrap_or(0);
|
||||
if app.ui.scripts.is_empty() {
|
||||
ui.label(egui::RichText::new("No scripts. Add one below.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "No scripts. Add one below.")).size(11.0).color(t.text_dim),
|
||||
));
|
||||
}
|
||||
let mut run: Option<usize> = None;
|
||||
for (i, (name, _)) in app.ui.scripts.iter().enumerate() {
|
||||
|
|
@ -156,11 +170,11 @@ pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
if ui.add_enabled(!app.ui.scripts.is_empty(), egui::Button::new("Run")).clicked() {
|
||||
run = Some(sel);
|
||||
}
|
||||
if ui.button("New").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New"))).clicked() {
|
||||
app.ui.scripts.push((format!("Script {}", app.ui.scripts.len() + 1), "# command.id {json} per line\n".into()));
|
||||
sel = app.ui.scripts.len() - 1;
|
||||
}
|
||||
if ui.button("Load…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Load…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("script"))
|
||||
{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.data_mut(|d| d.insert_temp(id, draft));
|
||||
ui.separator();
|
||||
if notes.is_empty() {
|
||||
ui.label(egui::RichText::new("No notes in this document.").size(11.0).color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "No notes in this document.")).size(11.0).color(t.text_dim),
|
||||
));
|
||||
}
|
||||
for n in notes {
|
||||
let (story, nid, at) = (n["story"].clone(), n["id"].clone(), n["at"].as_u64().unwrap_or(0));
|
||||
|
|
|
|||
|
|
@ -208,24 +208,29 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
|
||||
fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Document, abs: usize, si: usize) {
|
||||
if ui.button("Insert Page After").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Insert Page After"))).clicked() {
|
||||
let _ = app.run("layout.pages.insert", json!({"after": abs, "count": 1}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Delete Page").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Page"))).clicked() {
|
||||
let _ = app.run("layout.pages.delete", json!({"pages": [abs]}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Duplicate Spread").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Duplicate Spread"))).clicked() {
|
||||
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si}));
|
||||
ui.close();
|
||||
}
|
||||
if si > 0 && ui.button("Add to Previous Spread").on_hover_text("Spreads of up to 10 pages").clicked() {
|
||||
if si > 0
|
||||
&& ui
|
||||
.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Add to Previous Spread")))
|
||||
.on_hover_text("Spreads of up to 10 pages")
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("layout.pages.toSpread", json!({"page": abs + 1, "spread": si - 1}));
|
||||
ui.close();
|
||||
}
|
||||
let mut shuffle = doc.spreads.get(si).is_none_or(|sp| sp.allow_shuffle);
|
||||
if ui.checkbox(&mut shuffle, "Allow Spread to Shuffle").clicked() {
|
||||
if ui.checkbox(&mut shuffle, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Allow Spread to Shuffle"))).clicked() {
|
||||
let _ = app.run("layout.spreadShuffle", json!({"spread": si, "allow": shuffle}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -240,8 +245,8 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu
|
|||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.menu_button("Apply Parent", |ui| {
|
||||
if ui.button("[None]").clicked() {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[None]"))).clicked() {
|
||||
let _ = app.run("layout.pages.applyParent", json!({"pages": [abs], "parent": null}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,20 +49,20 @@ fn set_temp_bool(ui: &Ui, key: &str, v: bool) {
|
|||
fn selection_title(app: &DesignApp, info: &Option<SelInfo>) -> String {
|
||||
let Some(st) = app.session.active() else { return String::new() };
|
||||
if st.selection.text.is_some() {
|
||||
return "Characters".into();
|
||||
return crate::i18n::tr(&app.ui.language, "Characters").into();
|
||||
}
|
||||
match info {
|
||||
None => "No Selection".into(),
|
||||
Some(i) if i.count > 1 => "Multiple Objects".into(),
|
||||
None => crate::i18n::tr(&app.ui.language, "No Selection").into(),
|
||||
Some(i) if i.count > 1 => crate::i18n::tr(&app.ui.language, "Multiple Objects").into(),
|
||||
Some(i) => match i.kind {
|
||||
"<text frame>" => "Text Frame".into(),
|
||||
"<group>" => "Group".into(),
|
||||
"<image>" => "Image".into(),
|
||||
"<rectangle>" => "Rectangle".into(),
|
||||
"<ellipse>" => "Ellipse".into(),
|
||||
"<polygon>" => "Polygon".into(),
|
||||
"<line>" => "Line".into(),
|
||||
_ => "Path".into(),
|
||||
"<text frame>" => crate::i18n::tr(&app.ui.language, "Text Frame").into(),
|
||||
"<group>" => crate::i18n::tr(&app.ui.language, "Group").into(),
|
||||
"<image>" => crate::i18n::tr(&app.ui.language, "Image").into(),
|
||||
"<rectangle>" => crate::i18n::tr(&app.ui.language, "Rectangle").into(),
|
||||
"<ellipse>" => crate::i18n::tr(&app.ui.language, "Ellipse").into(),
|
||||
"<polygon>" => crate::i18n::tr(&app.ui.language, "Polygon").into(),
|
||||
"<line>" => crate::i18n::tr(&app.ui.language, "Line").into(),
|
||||
_ => crate::i18n::tr(&app.ui.language, "Path").into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ fn selection_title(app: &DesignApp, info: &Option<SelInfo>) -> String {
|
|||
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
if app.session.active().is_none() {
|
||||
ui.label(egui::RichText::new("No document open").color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No document open")).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
ui.spacing_mut().item_spacing = vec2(6.0, 5.0);
|
||||
|
|
@ -80,19 +80,19 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
if has_text {
|
||||
text_style_section(app, ui);
|
||||
divider(ui);
|
||||
section(ui, "Appearance", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Appearance"), true);
|
||||
text_appearance_section(app, ui);
|
||||
divider(ui);
|
||||
section(ui, "Character", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Character"), true);
|
||||
character_section(app, ui);
|
||||
divider(ui);
|
||||
section(ui, "Paragraph", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Paragraph"), true);
|
||||
paragraph_section(app, ui);
|
||||
divider(ui);
|
||||
section(ui, "Bullets and Numbering", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Bullets and Numbering"), true);
|
||||
bullets_section(app, ui);
|
||||
divider(ui);
|
||||
section(ui, "Quick Actions", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Quick Actions"), true);
|
||||
quick_actions_text(app, ui);
|
||||
return;
|
||||
}
|
||||
|
|
@ -100,34 +100,34 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
document_sections(app, ui);
|
||||
return;
|
||||
};
|
||||
section(ui, "Transform", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Transform"), true);
|
||||
transform_section(app, ui, &i);
|
||||
divider(ui);
|
||||
if i.is_text && i.count == 1 {
|
||||
text_style_section(app, ui);
|
||||
divider(ui);
|
||||
}
|
||||
section(ui, "Appearance", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Appearance"), true);
|
||||
appearance_section(app, ui, &i);
|
||||
divider(ui);
|
||||
if i.is_text && i.count == 1 {
|
||||
section(ui, "Character", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Character"), true);
|
||||
character_section(app, ui);
|
||||
divider(ui);
|
||||
section(ui, "Paragraph", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Paragraph"), true);
|
||||
paragraph_section(app, ui);
|
||||
divider(ui);
|
||||
}
|
||||
section(ui, "Align", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Align"), true);
|
||||
align_row(app, ui);
|
||||
divider(ui);
|
||||
if i.is_text && i.count == 1 {
|
||||
section(ui, "Text Frame", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Text Frame"), true);
|
||||
text_frame_section(app, ui);
|
||||
divider(ui);
|
||||
} else {
|
||||
if i.is_graphic {
|
||||
section(ui, "Frame Fitting", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Frame Fitting"), true);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
for (icon, mode, tip) in [
|
||||
|
|
@ -144,11 +144,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
divider(ui);
|
||||
}
|
||||
section(ui, "Text Wrap", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Text Wrap"), true);
|
||||
wrap_section(app, ui, &i);
|
||||
divider(ui);
|
||||
}
|
||||
section(ui, "Quick Actions", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Quick Actions"), true);
|
||||
quick_actions_object(app, ui, &i);
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
|||
let Some(st) = app.session.active() else { return };
|
||||
let d = st.doc.clone();
|
||||
let (pw, ph) = (d.settings.page_width, d.settings.page_height);
|
||||
section(ui, "Document", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Document"), true);
|
||||
// Preset dropdown + orientation.
|
||||
let b = block(ui, FIELD_H);
|
||||
let mut preset = None;
|
||||
|
|
@ -236,12 +236,14 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
|||
}
|
||||
}
|
||||
let mut facing = d.settings.facing_pages;
|
||||
let changed = place(ui, sub(b, 126.0, 26.0, 100.0, FIELD_H), |ui| widgets::checkbox(ui, &mut facing, "Facing Pages").changed());
|
||||
let changed = place(ui, sub(b, 126.0, 26.0, 100.0, FIELD_H), |ui| {
|
||||
widgets::checkbox(ui, &mut facing, crate::i18n::tr(&app.ui.language, "Facing Pages")).changed()
|
||||
});
|
||||
if changed {
|
||||
let _ = app.run("layout.documentSetup", json!({"facingPages": facing}));
|
||||
}
|
||||
// Margins.
|
||||
section(ui, "Margins", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Margins"), true);
|
||||
let cur = crate::canvas::current_page(app).unwrap_or(0);
|
||||
if let Some(p) = d.page(cur).cloned() {
|
||||
let linked = temp_bool(ui, "margins_linked", true);
|
||||
|
|
@ -289,12 +291,12 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
|||
}
|
||||
ui.add_space(6.0);
|
||||
let fw = full_width(ui);
|
||||
if outline_button(ui, "Adjust Layout", fw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Adjust Layout"), fw).clicked() {
|
||||
crate::dialogs::open_document_setup(app);
|
||||
}
|
||||
divider(ui);
|
||||
// Page.
|
||||
section(ui, "Page", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Page"), true);
|
||||
let b = block(ui, FIELD_H);
|
||||
let names: Vec<String> = (0..d.page_count()).map(|i| app.session.page_label(i)).collect();
|
||||
let picked = place(ui, sub(b, 0.0, 0.0, 93.0, FIELD_H), |ui| {
|
||||
|
|
@ -316,13 +318,13 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
|||
let _ = app.run("layout.pages.delete", json!({"pages": [cur]}));
|
||||
}
|
||||
ui.add_space(3.0);
|
||||
if outline_button(ui, "Edit Page", fw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Edit Page"), fw).clicked() {
|
||||
crate::dialogs::open_document_setup(app);
|
||||
}
|
||||
divider(ui);
|
||||
// Rulers & Grids / Guides: icon toggle rows.
|
||||
let (mut rulers, mut bl, mut dg) = (app.ui.rulers, app.ui.baseline_grid, app.ui.document_grid);
|
||||
section_row(ui, "Rulers & Grids", |ui| {
|
||||
section_row(ui, crate::i18n::tr(&app.ui.language, "Rulers & Grids"), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 14.0;
|
||||
if icon_toggle(ui, "grid-document", dg, "Show Document Grid").clicked() {
|
||||
dg = !dg;
|
||||
|
|
@ -343,7 +345,7 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
|||
app.ui.document_grid = dg;
|
||||
rule(ui);
|
||||
let (mut guides, mut locked, mut smart) = (app.ui.guides, app.ui.guides_locked, app.ui.smart_guides);
|
||||
section_row(ui, "Guides", |ui| {
|
||||
section_row(ui, crate::i18n::tr(&app.ui.language, "Guides"), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 14.0;
|
||||
if icon_toggle(ui, "guides-smart", smart, "Smart Guides").clicked() {
|
||||
smart = !smart;
|
||||
|
|
@ -363,8 +365,8 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
|||
}
|
||||
app.ui.guides_locked = locked;
|
||||
rule(ui);
|
||||
section(ui, "Quick Actions", true);
|
||||
let (a, b) = button_pair(ui, "Import File", "Insert Page");
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Quick Actions"), true);
|
||||
let (a, b) = button_pair(ui, crate::i18n::tr(&app.ui.language, "Import File"), crate::i18n::tr(&app.ui.language, "Insert Page"));
|
||||
if a {
|
||||
let _ = app.run("app.placeDialog", json!({}));
|
||||
}
|
||||
|
|
@ -730,7 +732,7 @@ fn wrap_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
|||
ui.add_space(2.0);
|
||||
let mut inv = i.wrap_invert;
|
||||
let enabled = i.wrap != "none";
|
||||
let r = ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, &mut inv, "Invert")).inner;
|
||||
let r = ui.add_enabled_ui(enabled, |ui| widgets::checkbox(ui, &mut inv, crate::i18n::tr(&app.ui.language, "Invert"))).inner;
|
||||
if r.changed() {
|
||||
let _ = app.run("object.textWrap", json!({"mode": i.wrap, "invert": inv}));
|
||||
}
|
||||
|
|
@ -785,24 +787,24 @@ fn quick_actions_object(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
|||
if i.is_text {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 10.0;
|
||||
let r = outline_button(ui, "Convert Shape", hw);
|
||||
let r = outline_button(ui, crate::i18n::tr(&app.ui.language, "Convert Shape"), hw);
|
||||
convert_shape_menu(app, &r);
|
||||
let r = outline_button(ui, "Arrange", hw);
|
||||
let r = outline_button(ui, crate::i18n::tr(&app.ui.language, "Arrange"), hw);
|
||||
arrange_menu(app, &r);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
if outline_button(ui, "Fill with Placeholder Text", fw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Fill with Placeholder Text"), fw).clicked() {
|
||||
let _ = app.run("type.fillWithPlaceholder", json!({}));
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
if outline_button(ui, "Edit in Story Editor", fw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Edit in Story Editor"), fw).clicked() {
|
||||
let _ = app.run("app.storyEditor", json!({}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 10.0;
|
||||
let r = outline_button(ui, "Arrange", hw);
|
||||
let r = outline_button(ui, crate::i18n::tr(&app.ui.language, "Arrange"), hw);
|
||||
arrange_menu(app, &r);
|
||||
if outline_button(ui, if i.count > 1 { "Group" } else { "Lock" }, hw).clicked() {
|
||||
let _ = app.run(if i.count > 1 { "object.group" } else { "object.lock" }, json!({}));
|
||||
|
|
@ -810,11 +812,11 @@ fn quick_actions_object(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
|||
});
|
||||
ui.add_space(4.0);
|
||||
if i.kind == "<group>" {
|
||||
if outline_button(ui, "Ungroup", fw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Ungroup"), fw).clicked() {
|
||||
let _ = app.run("object.ungroup", json!({}));
|
||||
}
|
||||
} else {
|
||||
let r = outline_button(ui, "Convert Shape", fw);
|
||||
let r = outline_button(ui, crate::i18n::tr(&app.ui.language, "Convert Shape"), fw);
|
||||
convert_shape_menu(app, &r);
|
||||
}
|
||||
}
|
||||
|
|
@ -823,7 +825,7 @@ fn quick_actions_object(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
|||
|
||||
fn text_style_section(app: &mut DesignApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
section(ui, "Text Style", true);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Text Style"), true);
|
||||
let Some(a) = text_attrs(app) else { return };
|
||||
let tab = app.ui.text_style_tab;
|
||||
let fw = full_width(ui);
|
||||
|
|
@ -1084,7 +1086,7 @@ fn bullets_section(app: &mut DesignApp, ui: &mut Ui) {
|
|||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
if outline_button(ui, "Options", 66.0).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Options"), 66.0).clicked() {
|
||||
app.ui.open_panel = Some("paragraph".into());
|
||||
}
|
||||
});
|
||||
|
|
@ -1096,7 +1098,7 @@ fn quick_actions_text(app: &mut DesignApp, ui: &mut Ui) {
|
|||
let hw = (fw - 10.0) / 2.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 10.0;
|
||||
let r = outline_button(ui, "Change Case", hw);
|
||||
let r = outline_button(ui, crate::i18n::tr(&app.ui.language, "Change Case"), hw);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
for (label, v) in [("UPPERCASE", "allCaps"), ("Small Caps", "smallCaps"), ("Normal", "normal")] {
|
||||
if ui.button(label).clicked() {
|
||||
|
|
@ -1105,12 +1107,12 @@ fn quick_actions_text(app: &mut DesignApp, ui: &mut Ui) {
|
|||
}
|
||||
}
|
||||
});
|
||||
if outline_button(ui, "Story Editor", hw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Story Editor"), hw).clicked() {
|
||||
let _ = app.run("app.storyEditor", json!({}));
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
if outline_button(ui, "Fill with Placeholder Text", fw).clicked() {
|
||||
if outline_button(ui, crate::i18n::tr(&app.ui.language, "Fill with Placeholder Text"), fw).clicked() {
|
||||
let _ = app.run("type.fillWithPlaceholder", json!({}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1136,7 +1138,7 @@ fn text_frame_section(app: &mut DesignApp, ui: &mut Ui) {
|
|||
let _ = app.run("object.textFrameOptions", json!({"gutter": v}));
|
||||
}
|
||||
let ow = (b.max.x - 8.0) - (b.min.x + 164.0);
|
||||
if place(ui, sub(b, 164.0, 0.5, ow, 20.0), |ui| outline_button(ui, "Options", ow)).clicked() {
|
||||
if place(ui, sub(b, 164.0, 0.5, ow, 20.0), |ui| outline_button(ui, crate::i18n::tr(&app.ui.language, "Options"), ow)).clicked() {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("textFrameOptions", json!({})));
|
||||
}
|
||||
}
|
||||
|
|
@ -1176,7 +1178,7 @@ const LANGUAGES: &[&str] = &[
|
|||
|
||||
pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(a) = text_attrs(app) else {
|
||||
ui.label("Select text or a text frame.");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Select text or a text frame."));
|
||||
return;
|
||||
};
|
||||
let c = a["chars"].clone();
|
||||
|
|
@ -1317,7 +1319,7 @@ pub fn open_type_menu(app: &mut DesignApp, ui: &mut egui::Ui, features: &[String
|
|||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Stylistic Sets", |ui| {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Stylistic Sets")), |ui| {
|
||||
let mask = otf::stylistic_sets(features);
|
||||
for n in 1..=20u32 {
|
||||
let mut on = mask & (1 << (n - 1)) != 0;
|
||||
|
|
@ -1331,7 +1333,7 @@ pub fn open_type_menu(app: &mut DesignApp, ui: &mut egui::Ui, features: &[String
|
|||
|
||||
pub fn paragraph_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(a) = text_attrs(app) else {
|
||||
ui.label("Select text or a text frame.");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Select text or a text frame."));
|
||||
return;
|
||||
};
|
||||
let u = units(app);
|
||||
|
|
@ -1388,21 +1390,21 @@ pub fn paragraph_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
ui.horizontal(|ui| {
|
||||
let mut h = p["hyphenate"].as_bool().unwrap_or(true);
|
||||
if ui.checkbox(&mut h, "Hyphenate").changed() {
|
||||
if ui.checkbox(&mut h, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Hyphenate"))).changed() {
|
||||
let _ = app.run("type.para", json!({"attrs": {"hyphenate": h}}));
|
||||
}
|
||||
let mut grid = p["gridAlign"].as_str() == Some("allLines");
|
||||
if ui.checkbox(&mut grid, "Align to baseline grid").changed() {
|
||||
if ui.checkbox(&mut grid, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Align to baseline grid"))).changed() {
|
||||
let _ = app.run("type.para", json!({"attrs": {"gridAlign": if grid { "allLines" } else { "none" }}}));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
caption(ui, "Composer");
|
||||
let single = p["composer"].as_str() == Some("singleLine");
|
||||
if ui.selectable_label(!single, "Paragraph").clicked() {
|
||||
if ui.selectable_label(!single, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Paragraph"))).clicked() {
|
||||
let _ = app.run("type.para", json!({"attrs": {"composer": "paragraph"}}));
|
||||
}
|
||||
if ui.selectable_label(single, "Single-line").clicked() {
|
||||
if ui.selectable_label(single, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Single-line"))).clicked() {
|
||||
let _ = app.run("type.para", json!({"attrs": {"composer": "singleLine"}}));
|
||||
}
|
||||
});
|
||||
|
|
@ -1456,7 +1458,7 @@ pub fn paragraph_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
/// Stroke panel: weight, cap, miter limit, join, alignment, type, arrowheads, gap colour.
|
||||
pub fn stroke_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(i) = sel_info(app) else {
|
||||
ui.label("Select an object.");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Select an object."));
|
||||
return;
|
||||
};
|
||||
let Some(st) = app.session.active().and_then(|d| d.selection.items.first().and_then(|id| d.doc.item(*id)).map(|it| it.stroke.clone())) else {
|
||||
|
|
@ -1646,12 +1648,12 @@ pub fn pathfinder_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Paths").strong());
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Paths")).strong());
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Make Compound").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Make Compound"))).clicked() {
|
||||
let _ = app.run("object.makeCompoundPath", json!({}));
|
||||
}
|
||||
if ui.button("Release").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Release"))).clicked() {
|
||||
let _ = app.run("object.releaseCompoundPath", json!({}));
|
||||
}
|
||||
});
|
||||
|
|
@ -1705,7 +1707,10 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(format!("Story {} ← story {}", l["story"], l["parent"]));
|
||||
if out {
|
||||
ui.label(egui::RichText::new("⚠ out of date").color(egui::Color32::from_rgb(230, 160, 40)));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "⚠ out of date")).color(egui::Color32::from_rgb(230, 160, 40)),
|
||||
));
|
||||
if ui.small_button("Update").clicked() {
|
||||
let _ = app.run("story.updateLink", json!({"story": l["story"]}));
|
||||
}
|
||||
|
|
@ -1730,15 +1735,15 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
};
|
||||
if rows.is_empty() {
|
||||
ui.label(egui::RichText::new("No placed graphics.").color(t.text_dim));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "No placed graphics.")).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
let sel_key = egui::Id::new("links_panel_sel");
|
||||
let mut chosen: Option<u64> = ui.data(|d| d.get_temp(sel_key));
|
||||
egui::Grid::new("links_grid").num_columns(3).striped(true).spacing([10.0, 4.0]).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("Name").color(t.text_dim).size(11.0));
|
||||
ui.label(egui::RichText::new("Status").color(t.text_dim).size(11.0));
|
||||
ui.label(egui::RichText::new("Page · PPI").color(t.text_dim).size(11.0));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Status")).color(t.text_dim).size(11.0));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Page · PPI")).color(t.text_dim).size(11.0));
|
||||
ui.end_row();
|
||||
for r in &rows {
|
||||
let aid = r["asset"].as_u64().unwrap_or(0);
|
||||
|
|
@ -1775,27 +1780,30 @@ pub fn links_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
let Some(aid) = chosen else {
|
||||
ui.label(egui::RichText::new("Choose a link.").color(t.text_dim).size(11.0));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Choose a link.")).color(t.text_dim).size(11.0));
|
||||
return;
|
||||
};
|
||||
if ui.button("Relink…").clicked()
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Relink…"))).clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|pick| pick("relink"))
|
||||
{
|
||||
let _ = app.run("links.relink", json!({"asset": aid, "path": path}));
|
||||
}
|
||||
if ui.button("Go To").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Go To"))).clicked() {
|
||||
let _ = app.run("links.goTo", json!({"asset": aid}));
|
||||
}
|
||||
if ui.button("Update").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Update"))).clicked() {
|
||||
let _ = app.run("links.update", json!({"asset": aid}));
|
||||
}
|
||||
if ui.button("Embed").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Embed"))).clicked() {
|
||||
let _ = app.run("links.embed", json!({"asset": aid}));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
// Choose any file in the folder: missing links are looked up there by name.
|
||||
if ui.button("Relink to Folder…").on_hover_text("Pick a file in the folder holding the moved graphics").clicked()
|
||||
if ui
|
||||
.button(crate::i18n::tr(&app.ui.language, "Relink to Folder…"))
|
||||
.on_hover_text("Pick a file in the folder holding the moved graphics")
|
||||
.clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|pick| pick("relink"))
|
||||
&& let Some(dir) = std::path::Path::new(&path).parent()
|
||||
{
|
||||
|
|
@ -1829,7 +1837,7 @@ pub fn info_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let Some(it) = st.selection.items.first().and_then(|i| st.doc.item(*i)).cloned() else {
|
||||
ui.label("Select an object.");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Select an object."));
|
||||
return;
|
||||
};
|
||||
use designcraft_color::BlendMode as B;
|
||||
|
|
@ -1867,7 +1875,7 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
divider(ui);
|
||||
let ds = it.effects.drop_shadow.clone();
|
||||
let mut on = ds.on;
|
||||
if ui.checkbox(&mut on, "Drop Shadow").changed() {
|
||||
if ui.checkbox(&mut on, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Drop Shadow"))).changed() {
|
||||
let _ = app.run("object.dropShadow", json!({"on": on}));
|
||||
}
|
||||
if ds.on {
|
||||
|
|
@ -1891,7 +1899,7 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.label("");
|
||||
caption(ui, "");
|
||||
let mut gl = ds.global_light;
|
||||
if ui.checkbox(&mut gl, "Use Global Light").changed() {
|
||||
if ui.checkbox(&mut gl, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Use Global Light"))).changed() {
|
||||
let _ = app.run("object.dropShadow", json!({"on": true, "globalLight": gl}));
|
||||
}
|
||||
ui.end_row();
|
||||
|
|
@ -1943,7 +1951,7 @@ pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
/// Window › Output › Attributes: overprint fill, stroke and gap; nonprinting.
|
||||
pub fn attributes_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let Some(it) = app.session.active().and_then(|d| d.selection.items.first().and_then(|id| d.doc.item(*id)).cloned()) else {
|
||||
ui.label("Select an object.");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Select an object."));
|
||||
return;
|
||||
};
|
||||
let rows = [
|
||||
|
|
@ -1980,7 +1988,7 @@ fn variable_font_axes(app: &mut DesignApp, ui: &mut egui::Ui, family: &str, styl
|
|||
let cur = |tag: &str, default: f32| face.coords.iter().find(|(t, _)| t == tag.as_bytes()).map_or(default, |c| c.1);
|
||||
let mut values: Vec<(String, f32)> = axes.iter().map(|a| (a.0.clone(), cur(&a.0, a.3))).collect();
|
||||
let mut changed = false;
|
||||
ui.label(egui::RichText::new("Variable Font").size(11.0));
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Variable Font")).size(11.0));
|
||||
egui::Grid::new("vf_axes").num_columns(2).spacing(vec2(6.0, 2.0)).show(ui, |ui| {
|
||||
for (i, (_, name, min, _, max)) in axes.iter().enumerate() {
|
||||
caption(ui, name);
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ fn list(app: &mut DesignApp, ui: &mut egui::Ui, para: bool) {
|
|||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("⌥-click clears overrides").size(10.5).color(t.text_disabled));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "⌥-click clears overrides")).size(10.5).color(t.text_disabled),
|
||||
));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icons::button(ui, "plus", 20.0, false, "Create New Style from Selection").clicked() {
|
||||
let cmd = if para { "style.paragraph.create" } else { "style.character.create" };
|
||||
|
|
@ -91,13 +94,13 @@ fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, curren
|
|||
let _ = app.run(cmd, json!({"name": n}));
|
||||
ui.close();
|
||||
}
|
||||
if current == Some(n) && ui.button("Break Link to Style").clicked() {
|
||||
if current == Some(n) && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Break Link to Style"))).clicked() {
|
||||
let _ = app.run("style.breakLink", json!({"kind": kind}));
|
||||
ui.close();
|
||||
}
|
||||
if !n.starts_with('[') {
|
||||
ui.menu_button("Move to Group", |ui| {
|
||||
if n.contains('/') && ui.button("[No Group]").clicked() {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Group")), |ui| {
|
||||
if n.contains('/') && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[No Group]"))).clicked() {
|
||||
let _ = app.run("style.group", json!({"kind": kind, "names": [n], "group": ""}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
ui.data_mut(|d| d.insert_temp(open_id, open));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Ungroup Color Group").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ungroup Color Group"))).clicked() {
|
||||
let _ = app.run("swatch.ungroupColorGroup", json!({"name": g.name}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -60,22 +60,25 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Right-click: stroke, groups").size(10.5).color(t.text_disabled));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Right-click: stroke, groups")).size(10.5).color(t.text_disabled),
|
||||
));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icons::button(ui, "plus", 20.0, false, "New Swatch").clicked() {
|
||||
let _ = app.run("swatch.create", json!({"color": {"c": 0, "m": 50, "y": 100, "k": 0}}));
|
||||
}
|
||||
ui.menu_button("☰", |ui| {
|
||||
ui.menu_button("Ink Manager", |ui| {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ink Manager")), |ui| {
|
||||
let Ok(l) = app.session.execute("ink.list", &json!({})) else { return };
|
||||
let mut all = l["allToProcess"].as_bool().unwrap_or(false);
|
||||
if ui.checkbox(&mut all, "All Spots to Process").changed() {
|
||||
if ui.checkbox(&mut all, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "All Spots to Process"))).changed() {
|
||||
let _ = app.run("ink.options", json!({"allToProcess": all}));
|
||||
}
|
||||
ui.separator();
|
||||
let inks = l["inks"].as_array().cloned().unwrap_or_default();
|
||||
if inks.is_empty() {
|
||||
ui.label("No spot inks");
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "No spot inks"));
|
||||
}
|
||||
for ink in inks {
|
||||
let name = ink["name"].as_str().unwrap_or("").to_string();
|
||||
|
|
@ -87,15 +90,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
}
|
||||
});
|
||||
if ui.button("New Color Group").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Color Group"))).clicked() {
|
||||
let _ = app.run("swatch.newColorGroup", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Load Swatches…").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Load Swatches…"))).clicked() {
|
||||
let _ = app.run("app.loadSwatches", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Save Swatches for Exchange…").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Save Swatches for Exchange…"))).clicked() {
|
||||
let _ = app.run("app.saveSwatches", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -132,7 +135,7 @@ fn swatch_row(
|
|||
let m = match color {
|
||||
designcraft_color::Color::Cmyk { .. } => "CMYK",
|
||||
designcraft_color::Color::Rgb { .. } => "RGB",
|
||||
designcraft_color::Color::Gray { .. } => "Gray",
|
||||
designcraft_color::Color::Gray { .. } => crate::i18n::tr(&app.ui.language, "Gray"),
|
||||
};
|
||||
format!("{}{}", if *color_type == ColorType::Spot { "● " } else { "" }, m)
|
||||
}
|
||||
|
|
@ -149,13 +152,13 @@ fn swatch_row(
|
|||
let _ = r;
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Apply to Stroke").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply to Stroke"))).clicked() {
|
||||
let _ = app.run("object.stroke", json!({"swatch": sw.name}));
|
||||
ui.close();
|
||||
}
|
||||
if !sw.locked {
|
||||
ui.menu_button("Move to Color Group", |ui| {
|
||||
if ui.button("(Top Level)").clicked() {
|
||||
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Move to Color Group")), |ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "(Top Level)"))).clicked() {
|
||||
let _ = app.run("swatch.moveToGroup", json!({"swatches": [sw.name], "group": null}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -166,12 +169,12 @@ fn swatch_row(
|
|||
}
|
||||
}
|
||||
});
|
||||
if ui.button("New Color Group with Swatch").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Color Group with Swatch"))).clicked() {
|
||||
let _ = app.run("swatch.newColorGroup", json!({"swatches": [sw.name]}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Delete Swatch").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Swatch"))).clicked() {
|
||||
let _ = app.run("swatch.delete", json!({"name": sw.name}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
@ -192,7 +195,10 @@ pub enum ColorMode {
|
|||
pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(i) = super::sel_info(app) else {
|
||||
ui.label(egui::RichText::new("Select an object to color its fill or stroke.").color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select an object to color its fill or stroke.")).color(t.text_dim),
|
||||
));
|
||||
return;
|
||||
};
|
||||
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return };
|
||||
|
|
@ -246,7 +252,10 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
let sw = doc.swatch(&name).cloned();
|
||||
let Some(c) = doc.resolve_color(&name, 1.0) else {
|
||||
ui.label(egui::RichText::new("[None] — choose a colour on the ramp.").color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "[None] — choose a colour on the ramp.")).color(t.text_dim),
|
||||
));
|
||||
spectrum(app, ui, target);
|
||||
return;
|
||||
};
|
||||
|
|
@ -388,13 +397,16 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let Some(it) = st.selection.items.first().and_then(|i| st.doc.item(*i)) else {
|
||||
ui.label(egui::RichText::new("Select an object to give its fill a gradient.").color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select an object to give its fill a gradient.")).color(t.text_dim),
|
||||
));
|
||||
return;
|
||||
};
|
||||
let fill = it.fill.clone();
|
||||
let Some(g) = designcraft_color::swatch::resolve_gradient(&st.doc.swatches, &fill.swatch).cloned() else {
|
||||
ui.label(egui::RichText::new("The fill isn't a gradient.").color(t.text_dim));
|
||||
if ui.button("Apply Gradient").clicked() {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "The fill isn't a gradient.")).color(t.text_dim));
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Gradient"))).clicked() {
|
||||
let _ = app.run("object.gradient", json!({}));
|
||||
}
|
||||
return;
|
||||
|
|
@ -409,7 +421,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
}
|
||||
});
|
||||
if ui.button("Reverse").on_hover_text("Reverse the gradient").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Reverse"))).on_hover_text("Reverse the gradient").clicked() {
|
||||
let _ = app.run("object.gradient", json!({"reverse": true}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ use crate::widgets::{caption, divider, measure, number};
|
|||
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Ok(info) = app.session.execute("table.get", &json!({})) else {
|
||||
ui.label(egui::RichText::new("Place the insertion point in a table, or create one.").color(t.text_dim));
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Place the insertion point in a table, or create one.")).color(t.text_dim),
|
||||
));
|
||||
ui.add_space(6.0);
|
||||
if ui.button("Create Table…").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Create Table…"))).clicked() {
|
||||
let _ = app.run("app.insertTableDialog", json!({}));
|
||||
}
|
||||
return;
|
||||
|
|
@ -49,7 +52,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
("Col ←", "table.moveColumn", c0, c0.saturating_sub(1), c0 > 0),
|
||||
("Col →", "table.moveColumn", c0, c0 + 1, c0 + 1 < nc),
|
||||
] {
|
||||
if ui.add_enabled(ok, egui::Button::new(label).small()).clicked()
|
||||
if ui.add_enabled(ok, egui::Button::new(crate::rtl::widget(ui, label)).small()).clicked()
|
||||
&& let Err(e) = app.run(id, json!({"from": from, "to": to}))
|
||||
{
|
||||
app.status(format!("Table: {e}"));
|
||||
|
|
@ -160,37 +163,37 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
divider(ui);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Merge Cells").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Merge Cells"))).clicked() {
|
||||
run(app, "table.merge", json!({}));
|
||||
}
|
||||
if ui.button("Unmerge").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Unmerge"))).clicked() {
|
||||
run(app, "table.unmerge", json!({}));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Insert Row").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Insert Row"))).clicked() {
|
||||
run(app, "table.insertRowBelow", json!({}));
|
||||
}
|
||||
if ui.button("Insert Column").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Insert Column"))).clicked() {
|
||||
run(app, "table.insertColumnRight", json!({}));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Delete Row").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Row"))).clicked() {
|
||||
run(app, "table.deleteRow", json!({}));
|
||||
}
|
||||
if ui.button("Delete Column").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Column"))).clicked() {
|
||||
run(app, "table.deleteColumn", json!({}));
|
||||
}
|
||||
});
|
||||
divider(ui);
|
||||
let mut alt = !info["options"]["altRows"].is_null();
|
||||
if ui.checkbox(&mut alt, "Alternating Row Fills").changed() {
|
||||
if ui.checkbox(&mut alt, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Alternating Row Fills"))).changed() {
|
||||
let v = if alt { json!({"first": 1, "firstColor": "[Black]", "firstTint": 0.1, "next": 1, "nextColor": "[None]"}) } else { Value::Null };
|
||||
run(app, "table.options", json!({"altRows": v}));
|
||||
}
|
||||
let mut rep = info["options"]["repeatHeader"].as_bool().unwrap_or(true);
|
||||
if ui.checkbox(&mut rep, "Repeat Header Rows").changed() {
|
||||
if ui.checkbox(&mut rep, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Repeat Header Rows"))).changed() {
|
||||
run(app, "table.options", json!({"repeatHeader": rep}));
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,315 @@
|
|||
//! Read-only interface text: Unicode bidi run ordering followed by Arabic shaping.
|
||||
//! Source strings stay logical. Never use these galleys for editable text/caret mapping.
|
||||
use egui::text::{LayoutJob, TextWrapping};
|
||||
use egui::{Align, Align2, Color32, FontFamily, FontId, Galley, Pos2, Rect, WidgetText};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use unicode_bidi::BidiInfo;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
/// Resolve translated labels through bidi, without changing IDs, commands or stored text.
|
||||
pub fn widget(ui: &egui::Ui, text: impl Into<WidgetText>) -> WidgetText {
|
||||
let text = text.into();
|
||||
if text.text().is_ascii() || !BidiInfo::new(text.text(), None).has_rtl() {
|
||||
return text;
|
||||
}
|
||||
let mut job = (*text.into_layout_job(ui.style(), egui::FontSelection::Default, Align::Center)).clone();
|
||||
job.wrap = TextWrapping::from_wrap_mode_and_width(ui.wrap_mode(), ui.available_width());
|
||||
WidgetText::Galley(layout(ui.ctx(), job))
|
||||
}
|
||||
|
||||
/// Labels are read-only: egui's LTR selection indices cannot map bidi glyph order.
|
||||
/// Accessibility still receives the original logical string through Galley::job.
|
||||
pub fn label(ui: &mut egui::Ui, text: impl Into<WidgetText>) -> egui::Response {
|
||||
let text = text.into();
|
||||
let rtl = !text.text().is_ascii() && BidiInfo::new(text.text(), None).has_rtl();
|
||||
if !rtl {
|
||||
return ui.label(text);
|
||||
}
|
||||
let selectable = false;
|
||||
let rendered = widget(ui, text.clone());
|
||||
let elided = matches!(&rendered, WidgetText::Galley(galley) if galley.elided);
|
||||
let response = ui.add(egui::Label::new(rendered).selectable(selectable).show_tooltip_when_elided(!rtl));
|
||||
if rtl && elided {
|
||||
response.on_hover_ui(|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
|
||||
label(ui, text);
|
||||
})
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// Isolate a user-provided name when embedding it in LTR chrome/metadata.
|
||||
/// A Latin product name or suffix must not change the name's internal reading order.
|
||||
pub fn isolate(text: &str) -> String {
|
||||
if text.chars().any(designcraft_fonts::is_rtl) { format!("\u{2068}{text}\u{2069}") } else { text.to_owned() }
|
||||
}
|
||||
|
||||
pub fn plain(ctx: &egui::Context, text: &str, font: FontId, color: Color32) -> Arc<Galley> {
|
||||
layout(ctx, LayoutJob::simple_singleline(text.to_owned(), font, color))
|
||||
}
|
||||
|
||||
pub fn paint(p: &egui::Painter, pos: Pos2, anchor: Align2, text: &str, font: FontId, color: Color32) {
|
||||
let galley = plain(p.ctx(), text, font, color);
|
||||
let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||
p.galley(rect.min, galley, color);
|
||||
}
|
||||
|
||||
/// Reorder directional runs, not characters: the shaper needs logical Arabic to join letters.
|
||||
fn visual_line(job: &LayoutJob, bidi: &BidiInfo<'_>, line: Range<usize>) -> LayoutJob {
|
||||
let mut visual = LayoutJob { break_on_newline: false, ..Default::default() };
|
||||
if line.is_empty() {
|
||||
if let Some(section) =
|
||||
job.sections.iter().find(|s| usize::from(s.byte_range.start) <= line.start && line.start <= usize::from(s.byte_range.end))
|
||||
{
|
||||
visual.append("", 0.0, section.format.clone());
|
||||
}
|
||||
return visual;
|
||||
}
|
||||
let Some(para) = bidi.paragraphs.iter().find(|p| p.range.start <= line.start && line.start < p.range.end) else {
|
||||
// Preserve the font/height for an empty line.
|
||||
if let Some(section) = job.sections.first() {
|
||||
visual.append("", 0.0, section.format.clone());
|
||||
}
|
||||
return visual;
|
||||
};
|
||||
let (levels, runs) = bidi.visual_runs(para, line);
|
||||
for run in runs {
|
||||
let rtl = levels[run.start].is_rtl();
|
||||
let mut sections: Vec<_> = job
|
||||
.sections
|
||||
.iter()
|
||||
.filter_map(|section| {
|
||||
let start = run.start.max(section.byte_range.start.into());
|
||||
let end = run.end.min(section.byte_range.end.into());
|
||||
(start < end).then_some((section, start..end))
|
||||
})
|
||||
.collect();
|
||||
if rtl {
|
||||
sections.reverse();
|
||||
}
|
||||
for (section, range) in sections {
|
||||
let mut format = section.format.clone();
|
||||
if rtl {
|
||||
let bold = format.font_id.family == FontFamily::Name("semibold".into());
|
||||
format.font_id.family = FontFamily::Name(if bold { "arabic-semibold" } else { "arabic" }.into());
|
||||
if bold {
|
||||
format.coords = egui::epaint::text::VariationCoords::new([(*b"wght", 600.0)]);
|
||||
}
|
||||
}
|
||||
let text = &job.text[range];
|
||||
if rtl && !text.chars().any(designcraft_fonts::is_rtl) {
|
||||
// Neutral-only RTL runs have no script for the shaper to infer a direction.
|
||||
// Apply UBA L4 mirroring here; Arabic runs are mirrored by the shaper.
|
||||
let neutral: String = text.chars().rev().map(|c| unicode_bidi_mirroring::get_mirrored(c).unwrap_or(c)).collect();
|
||||
visual.append(&neutral, section.leading_space, format);
|
||||
} else {
|
||||
visual.append(text, section.leading_space, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
visual
|
||||
}
|
||||
|
||||
fn layout(ctx: &egui::Context, mut job: LayoutJob) -> Arc<Galley> {
|
||||
if job.text.is_ascii() || !BidiInfo::new(&job.text, None).has_rtl() {
|
||||
return ctx.fonts_mut(|fonts| fonts.layout_job(job));
|
||||
}
|
||||
let original = Arc::new(job.clone());
|
||||
if !job.break_on_newline {
|
||||
// Single-line widgets show line separators as spaces; byte offsets stay unchanged.
|
||||
job.text = job.text.replace(['\n', '\r'], " ");
|
||||
}
|
||||
let bidi = BidiInfo::new(&job.text, None);
|
||||
let shape = |range| ctx.fonts_mut(|fonts| fonts.layout_job(visual_line(&job, &bidi, range)));
|
||||
// Wrap in logical reading order BEFORE applying bidi separately to each line.
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = 0;
|
||||
for paragraph in job.text.split_inclusive('\n') {
|
||||
let end = start + paragraph.len();
|
||||
let content_end = if paragraph.ends_with('\n') { end - 1 } else { end };
|
||||
if !job.wrap.max_width.is_finite() || shape(start..content_end).size().x <= job.wrap.max_width {
|
||||
ranges.push(start..content_end);
|
||||
start = end;
|
||||
continue;
|
||||
}
|
||||
let mut line_start = start;
|
||||
let mut word_break = start;
|
||||
for (offset, cluster) in job.text[start..content_end].grapheme_indices(true) {
|
||||
let index = start + offset;
|
||||
let boundary = index + cluster.len();
|
||||
if job.wrap.max_width.is_finite() && index > line_start && shape(line_start..boundary).size().x > job.wrap.max_width {
|
||||
let cut = if !job.wrap.break_anywhere && word_break > line_start { word_break } else { index };
|
||||
ranges.push(line_start..cut);
|
||||
line_start = cut;
|
||||
}
|
||||
if cluster.chars().all(char::is_whitespace) {
|
||||
word_break = boundary;
|
||||
}
|
||||
}
|
||||
ranges.push(line_start..content_end);
|
||||
start = end;
|
||||
}
|
||||
if job.text.ends_with('\n') {
|
||||
ranges.push(start..start);
|
||||
}
|
||||
let elided = ranges.len() > job.wrap.max_rows;
|
||||
ranges.truncate(job.wrap.max_rows);
|
||||
let mut lines = Vec::new();
|
||||
for (i, range) in ranges.iter().enumerate() {
|
||||
if elided && i + 1 == ranges.len() {
|
||||
// Append the ellipsis in logical order, then reorder and shape the final line.
|
||||
let mut last = LayoutJob::default();
|
||||
for section in &job.sections {
|
||||
let start = range.start.max(section.byte_range.start.into());
|
||||
let end = range.end.min(section.byte_range.end.into());
|
||||
if start < end {
|
||||
last.append(&job.text[start..end], 0.0, section.format.clone());
|
||||
}
|
||||
}
|
||||
let format = last.sections.last().or(job.sections.first()).map(|s| s.format.clone()).unwrap_or_default();
|
||||
let tail = job.wrap.overflow_character.map_or(String::new(), |c| c.to_string());
|
||||
let base = bidi.paragraphs.iter().find(|p| p.range.contains(&range.start)).map(|p| p.level);
|
||||
loop {
|
||||
let mut candidate = last.clone();
|
||||
candidate.append(&tail, 0.0, format.clone());
|
||||
let info = BidiInfo::new(&candidate.text, base);
|
||||
let shaped = ctx.fonts_mut(|fonts| fonts.layout_job(visual_line(&candidate, &info, 0..candidate.text.len())));
|
||||
if shaped.size().x <= job.wrap.max_width || last.text.is_empty() {
|
||||
lines.push(shaped);
|
||||
break;
|
||||
}
|
||||
let end = last.text.grapheme_indices(true).next_back().map_or(0, |(i, _)| i);
|
||||
last.text.truncate(end);
|
||||
for section in &mut last.sections {
|
||||
section.byte_range.end = usize::from(section.byte_range.end).min(end).into();
|
||||
}
|
||||
last.sections.retain(|s| s.byte_range.start < s.byte_range.end);
|
||||
}
|
||||
} else {
|
||||
lines.push(shape(range.clone()));
|
||||
}
|
||||
}
|
||||
let mut galley = Galley::concat(original, &lines, ctx.pixels_per_point());
|
||||
galley.elided = elided;
|
||||
// Each Arabic line shares the right edge; Latin runs and numbers remain LTR.
|
||||
let width = galley.rect.width();
|
||||
galley.mesh_bounds = Rect::NOTHING;
|
||||
for row in &mut galley.rows {
|
||||
row.pos.x += width - row.size.x;
|
||||
galley.mesh_bounds |= row.visuals.mesh_bounds.translate(row.pos.to_vec2());
|
||||
}
|
||||
Arc::new(galley)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn with_fonts(test: impl FnOnce(&egui::Context)) {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx, "ar");
|
||||
ctx.begin_pass(egui::RawInput { max_texture_side: Some(8192), ..Default::default() });
|
||||
test(&ctx);
|
||||
let mut output = ctx.end_pass();
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
|
||||
fn displayed(galley: &Galley) -> String {
|
||||
galley.rows.iter().flat_map(|row| row.glyphs.iter().filter(|g| g.advance_width > 0.0).map(|g| g.chr)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_arabic_latin_digits_and_brackets_preserve_logical_source() {
|
||||
with_fonts(|ctx| {
|
||||
let text = "ملف (PDF 123) جديد";
|
||||
let galley = plain(ctx, text, FontId::proportional(15.0), Color32::WHITE);
|
||||
assert_eq!(galley.job.text, text);
|
||||
let visual = displayed(&galley);
|
||||
assert!(visual.contains("PDF 123"), "{visual}");
|
||||
let row = &galley.rows[0];
|
||||
let left_bracket = row.glyphs.iter().find(|g| g.chr == ')' && !g.uv_rect.is_nothing()).unwrap();
|
||||
// A neutral-only final bracket must be mirrored even without an Arabic script run.
|
||||
let job = LayoutJob::simple_singleline("ملف (PDF 123)".to_owned(), FontId::proportional(15.0), Color32::WHITE);
|
||||
let bidi = BidiInfo::new(&job.text, None);
|
||||
let visual_job = visual_line(&job, &bidi, 0..job.text.len());
|
||||
assert!(visual_job.text.starts_with("(PDF 123"), "{}", visual_job.text);
|
||||
let p = row.glyphs.iter().find(|g| g.chr == 'P').unwrap();
|
||||
let three = row.glyphs.iter().find(|g| g.chr == '3').unwrap();
|
||||
assert!(left_bracket.pos.x < p.pos.x && p.pos.x < three.pos.x);
|
||||
assert!(visual.starts_with('د'), "Arabic final word belongs at the left: {visual}");
|
||||
assert!(galley.rect.is_finite());
|
||||
assert!(galley.mesh_bounds.is_finite());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolates_arabic_names_from_latin_brand_and_zoom_suffix() {
|
||||
with_fonts(|ctx| {
|
||||
for text in [format!("DesignCraft — {}", isolate("ملف (PDF 123) جديد")), format!("{} @ 89%", isolate("ملف (PDF 123) جديد"))]
|
||||
{
|
||||
let galley = plain(ctx, &text, FontId::proportional(15.0), Color32::WHITE);
|
||||
let glyphs = &galley.rows[0].glyphs;
|
||||
let x = |c| glyphs.iter().find(|g| g.chr == c && g.advance_width > 0.0).unwrap().pos.x;
|
||||
assert!(x('ج') < x('P') && x('P') < x('ف'), "Arabic name must keep its internal RTL order");
|
||||
assert_eq!(galley.job.text, text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joins_arabic_letters_using_one_font_including_spaces() {
|
||||
with_fonts(|ctx| {
|
||||
let text = "بِدء مُستند جَديد";
|
||||
let galley = plain(ctx, text, FontId::proportional(15.0), Color32::WHITE);
|
||||
let expected = ctx.fonts_mut(|fonts| {
|
||||
fonts.layout_job(LayoutJob::simple_singleline(text.to_owned(), FontId::new(15.0, FontFamily::Name("arabic".into())), Color32::WHITE))
|
||||
});
|
||||
assert_eq!(displayed(&galley), displayed(&expected));
|
||||
assert_eq!(galley.size(), expected.size());
|
||||
// Contextual glyphs (atlas positions) must match the unbroken Arabic shaping run.
|
||||
for (actual, expected) in galley.rows[0].glyphs.iter().zip(&expected.rows[0].glyphs) {
|
||||
assert_eq!(actual.uv_rect, expected.uv_rect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wraps_first_logical_word_first_and_right_aligns_lines() {
|
||||
with_fonts(|ctx| {
|
||||
let text = "مستند جديد للطباعة";
|
||||
let width = plain(ctx, "مستند ", FontId::proportional(15.0), Color32::WHITE).size().x + 1.0;
|
||||
let mut job = LayoutJob::simple_singleline(text.to_owned(), FontId::proportional(15.0), Color32::WHITE);
|
||||
job.wrap = TextWrapping::wrap_at_width(width);
|
||||
let galley = layout(ctx, job);
|
||||
assert!(galley.rows.len() >= 3);
|
||||
let first: String = galley.rows[0].glyphs.iter().map(|g| g.chr).collect();
|
||||
assert!(first.contains('م') && first.contains('س') && !first.contains('ج'), "{first}");
|
||||
for row in &galley.rows {
|
||||
assert!((row.pos.x + row.size.x - galley.size().x).abs() < 0.01);
|
||||
}
|
||||
assert_eq!(galley.job.text, text);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncates_long_words_and_handles_empty_lines() {
|
||||
with_fonts(|ctx| {
|
||||
let mut job = LayoutJob::simple_singleline("مستندجديدللطباعة\n\nPDF 123".to_owned(), FontId::proportional(15.0), Color32::WHITE);
|
||||
job.wrap = TextWrapping::truncate_at_width(45.0);
|
||||
let galley = layout(ctx, job);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert!(galley.elided);
|
||||
assert!(galley.size().x <= 45.0);
|
||||
assert!(displayed(&galley).contains('…'));
|
||||
let mut job = LayoutJob::simple_singleline("ملف\n\nجديد\n".to_owned(), FontId::proportional(15.0), Color32::WHITE);
|
||||
job.break_on_newline = true;
|
||||
let galley = layout(ctx, job);
|
||||
assert_eq!(galley.rows.len(), 4);
|
||||
assert!(galley.rect.is_finite());
|
||||
let galley = plain(ctx, "ملف\nجديد", FontId::proportional(15.0), Color32::WHITE);
|
||||
assert_eq!(galley.rows.len(), 1);
|
||||
assert_eq!(galley.job.text, "ملف\nجديد");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,10 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
|||
if let Some(o) = overset {
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("OVERSET").color(egui::Color32::from_rgb(230, 40, 40)).strong());
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "OVERSET")).color(egui::Color32::from_rgb(230, 40, 40)).strong(),
|
||||
));
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{} characters don't fit", story.text[o.min(story.text.len())..].chars().count()))
|
||||
.color(t.text_dim),
|
||||
|
|
|
|||
|
|
@ -85,7 +85,12 @@ fn text_controls(app: &mut DesignApp, ui: &mut Ui, in_text: bool) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
if !in_text {
|
||||
let (main, seg) = split_button(ui, "fit-text", "Fit Text");
|
||||
if main.on_hover_text("Fit frame height to text").clicked() {
|
||||
if main
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Fit frame height to text"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("object.textFrameOptions", json!({"autoSize": "heightOnly"}));
|
||||
}
|
||||
egui::Popup::menu(&seg).show(|ui| {
|
||||
|
|
@ -234,13 +239,15 @@ fn more(app: &mut DesignApp, ui: &mut Ui) {
|
|||
for k in 0..3 {
|
||||
ui.painter().circle_filled(pos2(r.min.x + 4.0 + k as f32 * 7.0, r.center().y), 1.8, t.icon);
|
||||
}
|
||||
let resp = resp.on_hover_text("More Options");
|
||||
let resp = resp.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "More Options"));
|
||||
});
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
if ui.button("Hide Bar").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Hide Bar"))).clicked() {
|
||||
app.ui.task_bar = false;
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Properties Panel").clicked() {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Properties Panel"))).clicked() {
|
||||
let _ = app.run("window.panel", json!({"panel": "properties"}));
|
||||
ui.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,14 +253,17 @@ impl Tokens {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn install_fonts(ctx: &egui::Context) {
|
||||
ctx.set_fonts(font_definitions(designcraft_fonts::CRAFT_FONTS));
|
||||
/// Install the UI fonts for the interface language `lang` (it orders the CJK fallbacks).
|
||||
pub fn install_fonts(ctx: &egui::Context, lang: &str) {
|
||||
ctx.set_fonts(font_definitions(designcraft_fonts::CRAFT_FONTS, lang));
|
||||
}
|
||||
|
||||
/// The UI fonts: the app's own, then the craft-fonts Japanese faces from `craft` (BIZ UDPGothic
|
||||
/// first; empty without `CRAFT_FONTS_DIR`) as a fallback at the end of every family. egui has
|
||||
/// no system-font discovery, so without craft-fonts Japanese UI text has no glyphs.
|
||||
fn font_definitions(craft: &'static [designcraft_fonts::CraftFont]) -> FontDefinitions {
|
||||
/// The UI fonts: the app's own, then the craft-fonts faces from `craft` (empty without
|
||||
/// `CRAFT_FONTS_DIR`) as fallbacks at the end of every family: Japanese (BIZ UDPGothic first) and
|
||||
/// Simplified Chinese, in the interface language's order, then Arabic. The `arabic` families
|
||||
/// put the Arabic face first for right-to-left runs. egui has no system-font discovery, so
|
||||
/// without craft-fonts CJK and Arabic UI text has no glyphs.
|
||||
fn font_definitions(craft: &'static [designcraft_fonts::CraftFont], lang: &str) -> FontDefinitions {
|
||||
let mut fonts = FontDefinitions::default();
|
||||
let add = |fonts: &mut FontDefinitions, name: &str, data: &'static [u8]| {
|
||||
fonts.font_data.insert(name.into(), Arc::new(FontData::from_static(data)));
|
||||
|
|
@ -271,20 +274,33 @@ fn font_definitions(craft: &'static [designcraft_fonts::CraftFont]) -> FontDefin
|
|||
fonts.families.entry(FontFamily::Proportional).or_default().insert(0, "ui".into());
|
||||
fonts.families.entry(FontFamily::Monospace).or_default().insert(0, "mono".into());
|
||||
fonts.families.insert(FontFamily::Name("semibold".into()), vec!["ui-semibold".into(), "ui".into()]);
|
||||
let mut japanese: Vec<&designcraft_fonts::CraftFont> = craft.iter().filter(|f| f.scripts.contains(&"Jpan")).collect();
|
||||
let pick = |script: &str| -> Vec<&designcraft_fonts::CraftFont> { craft.iter().filter(|f| f.scripts.contains(&script)).collect() };
|
||||
let mut japanese = pick("Jpan");
|
||||
// BIZ UDPGothic (the UI face) first, Regular before Bold; the semibold family prefers Bold.
|
||||
japanese.sort_by_key(|f| (f.family != "BIZ UDPGothic", f.style != "Regular"));
|
||||
let chinese = pick("Hans");
|
||||
let arabic = pick("Arab");
|
||||
let name = |f: &designcraft_fonts::CraftFont| format!("craft-{}-{}", f.family, f.style);
|
||||
for f in &japanese {
|
||||
for f in japanese.iter().chain(&chinese).chain(&arabic) {
|
||||
add(&mut fonts, &name(f), f.bytes);
|
||||
}
|
||||
// Han characters take the forms of the interface language: Chinese faces first for zh.
|
||||
let zh = lang.starts_with("zh");
|
||||
for (family, stack) in fonts.families.iter_mut() {
|
||||
let bold = *family == FontFamily::Name("semibold".into());
|
||||
let mut order = japanese.clone();
|
||||
let mut ja = japanese.clone();
|
||||
if bold {
|
||||
order.sort_by_key(|f| (f.family != "BIZ UDPGothic", f.style != "Bold"));
|
||||
ja.sort_by_key(|f| (f.family != "BIZ UDPGothic", f.style != "Bold"));
|
||||
}
|
||||
stack.extend(order.iter().map(|f| name(f)));
|
||||
let (first, second) = if zh { (&chinese, &ja) } else { (&ja, &chinese) };
|
||||
stack.extend(first.iter().chain(second).chain(&arabic).map(|f| name(f)));
|
||||
}
|
||||
// Arabic runs (rtl.rs): the Arabic face first, so letters and spaces shape as one run.
|
||||
for family in ["arabic", "arabic-semibold"] {
|
||||
let ui = if family == "arabic" { "ui" } else { "ui-semibold" };
|
||||
let stack = arabic.iter().map(|f| name(f)).chain([ui.to_string(), "ui".to_string()]).chain(japanese.iter().chain(&chinese).map(|f| name(f)));
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
fonts.families.insert(FontFamily::Name(family.into()), stack.filter(|n| seen.insert(n.clone())).collect());
|
||||
}
|
||||
fonts
|
||||
}
|
||||
|
|
@ -362,7 +378,7 @@ mod japanese_font_tests {
|
|||
|
||||
fn ctx_with(craft: &'static [designcraft_fonts::CraftFont]) -> egui::Context {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_fonts(super::font_definitions(craft));
|
||||
ctx.set_fonts(super::font_definitions(craft, "ja"));
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
output.textures_delta.clear();
|
||||
ctx
|
||||
|
|
@ -385,7 +401,7 @@ mod japanese_font_tests {
|
|||
}
|
||||
});
|
||||
// The UI face is BIZ UDPGothic, ahead of the Mincho faces.
|
||||
let defs = super::font_definitions(designcraft_fonts::CRAFT_FONTS);
|
||||
let defs = super::font_definitions(designcraft_fonts::CRAFT_FONTS, "ja");
|
||||
let stack = &defs.families[&egui::FontFamily::Proportional];
|
||||
assert_eq!(stack.iter().find(|n| n.starts_with("craft-")).map(String::as_str), Some("craft-BIZ UDPGothic-Regular"));
|
||||
}
|
||||
|
|
@ -405,6 +421,6 @@ mod japanese_font_tests {
|
|||
}
|
||||
});
|
||||
// And the real installer works with whatever this build has.
|
||||
super::install_fonts(&egui::Context::default());
|
||||
super::install_fonts(&egui::Context::default(), "ja");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,13 +182,25 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
let (row, _) = ui.allocate_exact_size(vec2(width, 11.0), Sense::hover());
|
||||
let def = egui::Rect::from_min_size(egui::pos2(x0, row.min.y), vec2(10.0, 10.0));
|
||||
icons::paint(ui.painter(), def, "default-colors", t.icon);
|
||||
if ui.interact(def, ui.id().with("proxy_default"), Sense::click()).on_hover_text("Default Fill and Stroke (D)").clicked() {
|
||||
if ui
|
||||
.interact(def, ui.id().with("proxy_default"), Sense::click())
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Default Fill and Stroke (D)"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("object.fill", json!({"swatch": "[None]"}));
|
||||
let _ = app.run("object.stroke", json!({"swatch": "[Black]"}));
|
||||
}
|
||||
let swap = egui::Rect::from_min_size(egui::pos2(x0 + 19.0, row.min.y), vec2(11.0, 11.0));
|
||||
icons::paint(ui.painter(), swap, "swap", t.icon);
|
||||
if ui.interact(swap, ui.id().with("proxy_swap_btn"), Sense::click()).on_hover_text("Swap Fill and Stroke (Shift+X)").clicked() {
|
||||
if ui
|
||||
.interact(swap, ui.id().with("proxy_swap_btn"), Sense::click())
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Swap Fill and Stroke (Shift+X)"));
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("object.fill", json!({"swatch": stroke_sw}));
|
||||
let _ = app.run("object.stroke", json!({"swatch": fill_sw}));
|
||||
}
|
||||
|
|
@ -228,8 +240,12 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
paint_stroke(p);
|
||||
paint_fill(p);
|
||||
}
|
||||
let fresp = ui.interact(fill_r, ui.id().with("proxy_fill"), Sense::click()).on_hover_text("Fill (X)");
|
||||
let sresp = ui.interact(stroke_r.translate(vec2(0.0, 0.0)), ui.id().with("proxy_stroke"), Sense::click()).on_hover_text("Stroke (X)");
|
||||
let fresp = ui.interact(fill_r, ui.id().with("proxy_fill"), Sense::click()).on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Fill (X)"));
|
||||
});
|
||||
let sresp = ui.interact(stroke_r.translate(vec2(0.0, 0.0)), ui.id().with("proxy_stroke"), Sense::click()).on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Stroke (X)"));
|
||||
});
|
||||
if fresp.clicked() || sresp.clicked() {
|
||||
let front =
|
||||
if stroke_front { !fresp.clicked() } else { sresp.clicked() && !fill_r.contains(sresp.interact_pointer_pos().unwrap_or_default()) };
|
||||
|
|
@ -258,7 +274,9 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
// Apply None well (flyout: Apply Color / Gradient / None).
|
||||
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_text("Apply None (/)");
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ pub fn outline_button(ui: &mut Ui, label: &str, width: f32) -> Response {
|
|||
p.rect_filled(r, 2.0, t.hover);
|
||||
}
|
||||
p.rect_stroke(r, 2.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
p.text(r.center(), egui::Align2::CENTER_CENTER, label, label_font(), t.text);
|
||||
crate::rtl::paint(p, r.center(), egui::Align2::CENTER_CENTER, label, label_font(), t.text);
|
||||
resp
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +349,7 @@ pub fn full_width(ui: &Ui) -> f32 {
|
|||
pub fn link_label(ui: &mut Ui, text: &str, width: f32) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::click());
|
||||
let g = ui.painter().layout_no_wrap(text.to_string(), label_font(), t.text);
|
||||
let g = crate::rtl::plain(ui.ctx(), text, label_font(), t.text);
|
||||
let pos = pos2(r.min.x, r.center().y - g.size().y / 2.0);
|
||||
let w = g.size().x;
|
||||
ui.painter().galley(pos, g, t.text);
|
||||
|
|
@ -373,14 +373,14 @@ pub fn more_options(ui: &mut Ui) -> Response {
|
|||
/// A small dim label (field captions like `X:` or `W:`).
|
||||
pub fn caption(ui: &mut Ui, s: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new(s).size(LABEL_SIZE).color(t.text));
|
||||
crate::rtl::label(ui, egui::RichText::new(s).size(LABEL_SIZE).color(t.text));
|
||||
}
|
||||
|
||||
/// A right-aligned caption of a fixed width (`W:` before a spinner).
|
||||
pub fn caption_w(ui: &mut Ui, s: &str, w: f32) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, FIELD_H), Sense::hover());
|
||||
ui.painter().text(pos2(r.max.x - 2.0, r.center().y), egui::Align2::RIGHT_CENTER, s, egui::FontId::proportional(11.5), t.text);
|
||||
crate::rtl::paint(ui.painter(), pos2(r.max.x - 2.0, r.center().y), egui::Align2::RIGHT_CENTER, s, egui::FontId::proportional(11.5), t.text);
|
||||
}
|
||||
|
||||
/// The selection-type line at the top of the Properties panel ("No Selection", "Rectangle", …):
|
||||
|
|
@ -388,7 +388,7 @@ pub fn caption_w(ui: &mut Ui, s: &str, w: f32) {
|
|||
pub fn selection_band(ui: &mut Ui, title: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 33.0), Sense::hover());
|
||||
ui.painter().text(pos2(r.min.x, r.center().y + 1.0), egui::Align2::LEFT_CENTER, title, crate::theme::semibold(11.5), t.text_strong);
|
||||
crate::rtl::paint(ui.painter(), pos2(r.min.x, r.center().y + 1.0), egui::Align2::LEFT_CENTER, title, crate::theme::semibold(11.5), t.text_strong);
|
||||
rule(ui);
|
||||
}
|
||||
|
||||
|
|
@ -398,7 +398,7 @@ pub fn section(ui: &mut Ui, title: &str, _default_open: bool) -> bool {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
ui.add_space(7.0);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 16.0), Sense::hover());
|
||||
ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, title, egui::FontId::proportional(11.5), t.text_strong);
|
||||
crate::rtl::paint(ui.painter(), r.left_center(), egui::Align2::LEFT_CENTER, title, egui::FontId::proportional(11.5), t.text_strong);
|
||||
ui.add_space(3.0);
|
||||
true
|
||||
}
|
||||
|
|
@ -411,7 +411,7 @@ pub fn section_row(ui: &mut Ui, title: &str, add: impl FnOnce(&mut Ui)) {
|
|||
ui.horizontal(|ui| {
|
||||
ui.set_min_height(23.0);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(100.0, 23.0), Sense::hover());
|
||||
ui.painter().text(r.left_center(), egui::Align2::LEFT_CENTER, title, egui::FontId::proportional(11.5), t.text_strong);
|
||||
crate::rtl::paint(ui.painter(), r.left_center(), egui::Align2::LEFT_CENTER, title, egui::FontId::proportional(11.5), t.text_strong);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(4.0);
|
||||
add(ui)
|
||||
|
|
|
|||
Loading…
Reference in New Issue