feat: add multi-language (i18n) support with Japanese UI
This commit is contained in:
1 parent
fc8b71956c
commit
3afd6ae447
11 files changed
+647
-80
No files matched your search
@@ -169,6 +169,7 @@ fn main() -> eframe::Result {
|
|||||||
"SolveCraft",
|
"SolveCraft",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
|
solvecraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
||||||
let mut app = SolveApp::new(Session::default(), services());
|
let mut app = SolveApp::new(Session::default(), services());
|
||||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||||
|
|||||||
@@ -351,7 +351,8 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
|
|||||||
x += 21.0;
|
x += 21.0;
|
||||||
}
|
}
|
||||||
let col = row.color.unwrap_or(if row.dim { t.text_dim } else { t.text });
|
let col = row.color.unwrap_or(if row.dim { t.text_dim } else { t.text });
|
||||||
let galley = ui.painter().layout_no_wrap(row.label.to_string(), FontId::proportional(12.5), col);
|
let label_display = crate::i18n::tr_dyn(row.label);
|
||||||
|
let galley = ui.painter().layout_no_wrap(label_display, FontId::proportional(12.5), col);
|
||||||
let lw = galley.size().x;
|
let lw = galley.size().x;
|
||||||
ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
|
ui.painter().galley(pos2(x, cy - galley.size().y / 2.0), galley, col);
|
||||||
let mut bx = (x + lw + 6.0).min(r.right() - 16.0);
|
let mut bx = (x + lw + 6.0).min(r.right() - 16.0);
|
||||||
@@ -362,9 +363,9 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
|
|||||||
}
|
}
|
||||||
crate::scenario::publish_handle(&format!("row:{}", row.label), r.center());
|
crate::scenario::publish_handle(&format!("row:{}", row.label), r.center());
|
||||||
let tip = match hover_on {
|
let tip = match hover_on {
|
||||||
1 => Some(if row.fold == Some(true) { "Collapse" } else { "Expand" }),
|
1 => Some(if row.fold == Some(true) { crate::i18n::tr("Collapse") } else { crate::i18n::tr("Expand") }),
|
||||||
2 => Some(if row.eye == Some(true) { "Hide" } else { "Show" }),
|
2 => Some(if row.eye == Some(true) { crate::i18n::tr("Hide") } else { crate::i18n::tr("Show") }),
|
||||||
3 => Some(if row.radio == Some(true) { "Active component" } else { "Activate" }),
|
3 => Some(if row.radio == Some(true) { crate::i18n::tr("Active component") } else { crate::i18n::tr("Activate") }),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let resp = match tip {
|
let resp = match tip {
|
||||||
|
|||||||
@@ -1273,7 +1273,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
painter.circle_filled(c, 3.0, t.text_dim);
|
painter.circle_filled(c, 3.0, t.text_dim);
|
||||||
}
|
}
|
||||||
for (k, item) in radial.iter().enumerate() {
|
for (k, item) in radial.iter().enumerate() {
|
||||||
let galley = painter.layout_no_wrap(item.label.clone(), font.clone(), if item.enabled { t.text } else { t.text_dim });
|
let display_label = crate::i18n::tr(&item.label);
|
||||||
|
let galley = painter.layout_no_wrap(display_label.to_string(), font.clone(), if item.enabled { t.text } else { t.text_dim });
|
||||||
let size = vec2(galley.size().x + 40.0, 28.0);
|
let size = vec2(galley.size().x + 40.0, 28.0);
|
||||||
let rr = slot_rect(c, k, size);
|
let rr = slot_rect(c, k, size);
|
||||||
let resp = ui.interact(rr, ui.id().with(("radial", k)), Sense::click());
|
let resp = ui.interact(rr, ui.id().with(("radial", k)), Sense::click());
|
||||||
@@ -1295,7 +1296,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
picked = Some(item.clone());
|
picked = Some(item.clone());
|
||||||
}
|
}
|
||||||
if item.enabled {
|
if item.enabled {
|
||||||
resp.on_hover_text(item.shortcut.clone());
|
resp.on_hover_text(crate::i18n::tr(&item.shortcut).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1393,7 +1394,7 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
|
|||||||
ui.painter().text(
|
ui.painter().text(
|
||||||
pos2(r.left() + 8.0, r.center().y + 2.0),
|
pos2(r.left() + 8.0, r.center().y + 2.0),
|
||||||
Align2::LEFT_CENTER,
|
Align2::LEFT_CENTER,
|
||||||
item.label.to_uppercase(),
|
crate::i18n::tr(&item.label).to_uppercase(),
|
||||||
FontId::proportional(10.5),
|
FontId::proportional(10.5),
|
||||||
t.text_dim,
|
t.text_dim,
|
||||||
);
|
);
|
||||||
@@ -1415,9 +1416,11 @@ pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let text = if item.enabled { t.text } else { t.text_dim.gamma_multiply(0.75) };
|
let text = if item.enabled { t.text } else { t.text_dim.gamma_multiply(0.75) };
|
||||||
ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, &item.label, FontId::proportional(13.0), text);
|
let display_label = crate::i18n::tr(&item.label);
|
||||||
|
ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, display_label, FontId::proportional(13.0), text);
|
||||||
if !item.shortcut.is_empty() {
|
if !item.shortcut.is_empty() {
|
||||||
ui.painter().text(pos2(r.right() - 10.0, r.center().y), Align2::RIGHT_CENTER, &item.shortcut, FontId::proportional(11.5), t.text_dim);
|
let display_shortcut = crate::i18n::tr(&item.shortcut);
|
||||||
|
ui.painter().text(pos2(r.right() - 10.0, r.center().y), Align2::RIGHT_CENTER, display_shortcut, FontId::proportional(11.5), t.text_dim);
|
||||||
}
|
}
|
||||||
(item.enabled && resp.clicked()).then(|| item.clone())
|
(item.enabled && resp.clicked()).then(|| item.clone())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -745,9 +745,10 @@ pub(crate) fn field(ui: &mut egui::Ui, s: &mut String) -> bool {
|
|||||||
|
|
||||||
pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usize) {
|
pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usize) {
|
||||||
let row = ROW.with(|r| r.borrow().clone());
|
let row = ROW.with(|r| r.borrow().clone());
|
||||||
let r = egui::ComboBox::from_id_salt(id).selected_text(labels.get(*sel).copied().unwrap_or("")).width(FIELD_W).show_ui(ui, |ui| {
|
let selected = labels.get(*sel).copied().map(tr).unwrap_or("");
|
||||||
|
let r = egui::ComboBox::from_id_salt(id).selected_text(selected).width(FIELD_W).show_ui(ui, |ui| {
|
||||||
for (i, l) in labels.iter().enumerate() {
|
for (i, l) in labels.iter().enumerate() {
|
||||||
let item = ui.selectable_value(sel, i, *l);
|
let item = ui.selectable_value(sel, i, tr(l));
|
||||||
crate::scenario::publish_handle(&format!("dialog:{row}:{l}"), item.rect.center());
|
crate::scenario::publish_handle(&format!("dialog:{row}:{l}"), item.rect.center());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -757,6 +758,11 @@ pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usiz
|
|||||||
thread_local! {
|
thread_local! {
|
||||||
/// The label of the dialog row being laid out (its widgets publish `dialog:<label>` handles).
|
/// The label of the dialog row being laid out (its widgets publish `dialog:<label>` handles).
|
||||||
static ROW: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
static ROW: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||||
|
static LANG: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tr(text: &str) -> &str {
|
||||||
|
crate::i18n::tr(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A dialog row's widget, for scenarios and demos: the handle `dialog:<row label>`.
|
/// A dialog row's widget, for scenarios and demos: the handle `dialog:<row label>`.
|
||||||
@@ -848,11 +854,12 @@ const LABEL_MAX_W: f32 = 150.0;
|
|||||||
/// A row label in the dialog's label column (the grid sizes the column to its widest label).
|
/// A row label in the dialog's label column (the grid sizes the column to its widest label).
|
||||||
pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) {
|
pub(crate) fn row_label(ui: &mut egui::Ui, text: &str) {
|
||||||
ROW.with(|r| *r.borrow_mut() = text.to_string());
|
ROW.with(|r| *r.borrow_mut() = text.to_string());
|
||||||
let full = egui::WidgetText::from(text).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x;
|
let display_text = tr(text);
|
||||||
|
let full = egui::WidgetText::from(display_text).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x;
|
||||||
let w = full.clamp(LABEL_W, LABEL_MAX_W);
|
let w = full.clamp(LABEL_W, LABEL_MAX_W);
|
||||||
ui.allocate_ui_with_layout(vec2(w, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
ui.allocate_ui_with_layout(vec2(w, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||||
ui.set_width(w);
|
ui.set_width(w);
|
||||||
ui.add(egui::Label::new(text).wrap());
|
ui.add(egui::Label::new(display_text).wrap());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,9 +1033,9 @@ fn input_rows(d: &mut Dialog, ui: &mut egui::Ui) {
|
|||||||
// A chip: "↖ Select" (outlined) while empty, "N selected" (filled) once picked.
|
// A chip: "↖ Select" (outlined) while empty, "N selected" (filled) once picked.
|
||||||
let filled = !inp.items.is_empty();
|
let filled = !inp.items.is_empty();
|
||||||
let text = if filled {
|
let text = if filled {
|
||||||
RichText::new(format!("↖ {} selected", inp.items.len())).color(Color32::WHITE)
|
RichText::new(format!("↖ {} {}", inp.items.len(), tr("selected"))).color(Color32::WHITE)
|
||||||
} else {
|
} else {
|
||||||
RichText::new("↖ Select").color(t.text)
|
RichText::new(format!("↖ {}", tr("Select"))).color(t.text)
|
||||||
};
|
};
|
||||||
let stroke = if active { Stroke::new(1.5, t.accent) } else { Stroke::new(1.0, t.dialog_border.gamma_multiply(1.6)) };
|
let stroke = if active { Stroke::new(1.5, t.accent) } else { Stroke::new(1.0, t.dialog_border.gamma_multiply(1.6)) };
|
||||||
// Not in the Tab order: Tab moves between values.
|
// Not in the Tab order: Tab moves between values.
|
||||||
@@ -1208,6 +1215,7 @@ fn auto_operation(app: &SolveApp, d: &mut Dialog) {
|
|||||||
/// the view cube, sized to its content.
|
/// the view cube, sized to its content.
|
||||||
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||||
let Some(mut d) = app.dialog.take() else { return };
|
let Some(mut d) = app.dialog.take() else { return };
|
||||||
|
LANG.with(|l| *l.borrow_mut() = app.preferences.language.clone());
|
||||||
auto_operation(app, &mut d);
|
auto_operation(app, &mut d);
|
||||||
update_measure(app, &mut d);
|
update_measure(app, &mut d);
|
||||||
update_fill_cells(app, &mut d);
|
update_fill_cells(app, &mut d);
|
||||||
@@ -1220,7 +1228,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let mut cancel = false;
|
let mut cancel = false;
|
||||||
let mut enter = false;
|
let mut enter = false;
|
||||||
let wide = d.is_wide();
|
let wide = d.is_wide();
|
||||||
let heading = if d.editing.is_some() { format!("EDIT {}", title(&d.kind)) } else { title(&d.kind).to_string() };
|
let raw_title = title(&d.kind);
|
||||||
|
let heading = if d.editing.is_some() { format!("{} {}", tr("EDIT"), tr(raw_title)) } else { tr(raw_title).to_string() };
|
||||||
let frame = egui::Frame::window(&ctx.global_style())
|
let frame = egui::Frame::window(&ctx.global_style())
|
||||||
.fill(t.dialog_bg)
|
.fill(t.dialog_bg)
|
||||||
.stroke(Stroke::new(1.0, t.dialog_border))
|
.stroke(Stroke::new(1.0, t.dialog_border))
|
||||||
@@ -1807,17 +1816,19 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let buttons = if has_ok { 64.0 * 2.0 + ui.spacing().item_spacing.x } else { 64.0 };
|
let buttons = if has_ok { 64.0 * 2.0 + ui.spacing().item_spacing.x } else { 64.0 };
|
||||||
crate::frame::right_aligned(ui, total, buttons, |ui| {
|
crate::frame::right_aligned(ui, total, buttons, |ui| {
|
||||||
if has_ok {
|
if has_ok {
|
||||||
let label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
|
let raw_label = if matches!(d.kind, Kind::ConfirmDelete { .. }) { "Delete" } else { "OK" };
|
||||||
|
let label = tr(raw_label);
|
||||||
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
|
let b = egui::Button::new(RichText::new(label).color(if valid { Color32::WHITE } else { t.text_dim }))
|
||||||
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
|
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
|
||||||
.min_size(vec2(64.0, 24.0));
|
.min_size(vec2(64.0, 24.0));
|
||||||
let r = ui.add_enabled(valid, b);
|
let r = ui.add_enabled(valid, b);
|
||||||
crate::scenario::publish_handle(&format!("dialog:{label}"), r.rect.center());
|
crate::scenario::publish_handle(&format!("dialog:{raw_label}"), r.rect.center());
|
||||||
if r.clicked() {
|
if r.clicked() {
|
||||||
ok = true;
|
ok = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. }) { "Close" } else { "Cancel" };
|
let raw_close = if matches!(d.kind, Kind::Params { .. } | Kind::Measure { .. }) { "Close" } else { "Cancel" };
|
||||||
|
let close = tr(raw_close);
|
||||||
if ui.add(egui::Button::new(close).min_size(vec2(64.0, 24.0))).clicked() {
|
if ui.add(egui::Button::new(close).min_size(vec2(64.0, 24.0))).clicked() {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,7 +307,14 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
|
|||||||
t.app_bar
|
t.app_bar
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ui.painter().text(home.center(), Align2::CENTER_CENTER, "Home", FontId::proportional(13.0), if app.home.open { t.text } else { t.app_bar_text });
|
let home_text = app.tr("Home");
|
||||||
|
ui.painter().text(
|
||||||
|
home.center(),
|
||||||
|
Align2::CENTER_CENTER,
|
||||||
|
home_text,
|
||||||
|
FontId::proportional(13.0),
|
||||||
|
if app.home.open { t.text } else { t.app_bar_text },
|
||||||
|
);
|
||||||
crate::scenario::publish_handle("tab:home", home.center());
|
crate::scenario::publish_handle("tab:home", home.center());
|
||||||
if response.clicked() {
|
if response.clicked() {
|
||||||
app.home.show(app.session.revision);
|
app.home.show(app.session.revision);
|
||||||
@@ -342,7 +349,8 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
|
|||||||
};
|
};
|
||||||
ui.painter().rect_filled(tr, egui::CornerRadius { nw: 5, ne: 5, sw: 0, se: 0 }, fill);
|
ui.painter().rect_filled(tr, egui::CornerRadius { nw: 5, ne: 5, sw: 0, se: 0 }, fill);
|
||||||
let ink = if active { t.text } else { t.app_bar_text };
|
let ink = if active { t.text } else { t.app_bar_text };
|
||||||
let label = format!("{name}{}", if *dirty { " •" } else { "" });
|
let name_display = if name == "Untitled" { app.tr("Untitled") } else { name };
|
||||||
|
let label = format!("{name_display}{}", if *dirty { " •" } else { "" });
|
||||||
let g = ui.painter().layout_no_wrap(label, FontId::proportional(13.0), ink);
|
let g = ui.painter().layout_no_wrap(label, FontId::proportional(13.0), ink);
|
||||||
let text_w = (tr.width() - 30.0).max(10.0);
|
let text_w = (tr.width() - 30.0).max(10.0);
|
||||||
let clip = Rect::from_min_size(tr.min, vec2(text_w + 10.0, tr.height()));
|
let clip = Rect::from_min_size(tr.min, vec2(text_w + 10.0, tr.height()));
|
||||||
@@ -447,23 +455,33 @@ pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
quit_step(app);
|
quit_step(app);
|
||||||
let Some(i) = app.docs.closing else { return };
|
let Some(i) = app.docs.closing else { return };
|
||||||
let name = tabs_info(app).get(i).map(|x| x.0.clone()).unwrap_or_default();
|
let name = tabs_info(app).get(i).map(|x| x.0.clone()).unwrap_or_default();
|
||||||
|
let name_display = if name == "Untitled" { crate::i18n::tr("Untitled") } else { &name };
|
||||||
let mut answer: Option<Answer> = None;
|
let mut answer: Option<Answer> = None;
|
||||||
crate::frame::window(ctx, format!("Save changes to {name}?"), crate::frame::Width::Normal)
|
let title = if crate::i18n::current_lang() == "ja" {
|
||||||
|
format!("{name_display} への変更を保存しますか?")
|
||||||
|
} else {
|
||||||
|
format!("Save changes to {name}?")
|
||||||
|
};
|
||||||
|
crate::frame::window(ctx, title, crate::frame::Width::Normal)
|
||||||
.id(egui::Id::new("sc_close_prompt"))
|
.id(egui::Id::new("sc_close_prompt"))
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
.resizable(false)
|
.resizable(false)
|
||||||
.anchor(Align2::CENTER_CENTER, vec2(0.0, 0.0))
|
.anchor(Align2::CENTER_CENTER, vec2(0.0, 0.0))
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.label(format!("{name} has unsaved changes. If you don't save, they are lost."));
|
if crate::i18n::current_lang() == "ja" {
|
||||||
|
ui.label(format!("{name_display} には保存されていない変更があります。保存しない場合、変更内容は失われます。"));
|
||||||
|
} else {
|
||||||
|
ui.label(format!("{name} has unsaved changes. If you don't save, they are lost."));
|
||||||
|
}
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Save").clicked() {
|
if ui.button(crate::i18n::tr("Save")).clicked() {
|
||||||
answer = Some(Answer::Save);
|
answer = Some(Answer::Save);
|
||||||
}
|
}
|
||||||
if ui.button("Don't Save").clicked() {
|
if ui.button(crate::i18n::tr("Don't Save")).clicked() {
|
||||||
answer = Some(Answer::DontSave);
|
answer = Some(Answer::DontSave);
|
||||||
}
|
}
|
||||||
if ui.button("Cancel").clicked() {
|
if ui.button(crate::i18n::tr("Cancel")).clicked() {
|
||||||
answer = Some(Answer::Cancel);
|
answer = Some(Answer::Cancel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//! System font loading and fallback setup for multi-language (CJK/Japanese) support.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Candidate font paths to look for on different operating systems.
|
||||||
|
const FONT_CANDIDATES: &[&str] = &[
|
||||||
|
// Windows
|
||||||
|
"C:\\Windows\\Fonts\\meiryo.ttc",
|
||||||
|
"C:\\Windows\\Fonts\\YuGothM.ttc",
|
||||||
|
"C:\\Windows\\Fonts\\YuGothR.ttc",
|
||||||
|
"C:\\Windows\\Fonts\\msgothic.ttc",
|
||||||
|
"C:\\Windows\\Fonts\\BIZ-UDGothicR.ttc",
|
||||||
|
// macOS
|
||||||
|
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||||
|
"/System/Library/Fonts/PingFang.ttc",
|
||||||
|
"/Library/Fonts/Arial Unicode.ttf",
|
||||||
|
// Linux
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Configure font definitions with system CJK fallback if available.
|
||||||
|
pub fn setup_fonts(ctx: &egui::Context) {
|
||||||
|
let mut font_bytes: Option<Vec<u8>> = None;
|
||||||
|
for path in FONT_CANDIDATES {
|
||||||
|
if let Ok(bytes) = std::fs::read(path) {
|
||||||
|
font_bytes = Some(bytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(bytes) = font_bytes else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
|
fonts.font_data.insert("cjk_fallback".to_owned(), Arc::new(egui::FontData::from_owned(bytes)));
|
||||||
|
|
||||||
|
fonts.families.entry(egui::FontFamily::Proportional).or_default().push("cjk_fallback".to_owned());
|
||||||
|
|
||||||
|
fonts.families.entry(egui::FontFamily::Monospace).or_default().push("cjk_fallback".to_owned());
|
||||||
|
|
||||||
|
ctx.set_fonts(fonts);
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
//! Multi-language (i18n) static translation dictionary for SolveCraft.
|
||||||
|
//!
|
||||||
|
//! Provides zero-overhead, completely offline local string translations.
|
||||||
|
|
||||||
|
/// Supported languages: (id, display_name).
|
||||||
|
pub const LANGUAGES: [(&str, &str); 2] = [("en", "English"), ("ja", "日本語")];
|
||||||
|
|
||||||
|
/// English to Japanese dictionary.
|
||||||
|
const JA_TRANSLATIONS: &[(&str, &str)] = &[
|
||||||
|
// --- Common & Window controls ---
|
||||||
|
("Preferences", "環境設定"),
|
||||||
|
("General", "一般"),
|
||||||
|
("Navigation", "ナビゲーション"),
|
||||||
|
("Keyboard Shortcuts", "キーボードショートカット"),
|
||||||
|
("Design", "デザイン"),
|
||||||
|
("Graphics", "グラフィックス"),
|
||||||
|
("Sketch", "スケッチ"),
|
||||||
|
("Selection", "選択"),
|
||||||
|
("Language", "言語"),
|
||||||
|
("Theme", "テーマ"),
|
||||||
|
("UI scale", "UIスケール"),
|
||||||
|
("Units of new designs", "新規デザインの単位"),
|
||||||
|
("Autosave every", "自動保存の間隔"),
|
||||||
|
("Length decimals", "長さの小数点桁数"),
|
||||||
|
("Angle decimals", "角度の小数点桁数"),
|
||||||
|
("Agent cursor", "AIエージェントカーソル"),
|
||||||
|
("Agent cursor speed", "AIエージェント速度"),
|
||||||
|
("Restore Defaults", "初期値に戻す"),
|
||||||
|
("Reset the settings on this page", "このページの設定をリセット"),
|
||||||
|
("OK", "OK"),
|
||||||
|
("Cancel", "キャンセル"),
|
||||||
|
("Apply", "適用"),
|
||||||
|
("Save", "保存"),
|
||||||
|
("Don't Save", "保存しない"),
|
||||||
|
("Untitled", "無題"),
|
||||||
|
("Dark", "ダーク"),
|
||||||
|
("Light", "ライト"),
|
||||||
|
("System", "システム"),
|
||||||
|
("Help", "ヘルプ"),
|
||||||
|
("About", "SolveCraft について"),
|
||||||
|
("Close", "閉じる"),
|
||||||
|
("Delete", "削除"),
|
||||||
|
("Expand", "展開"),
|
||||||
|
("Collapse", "折りたたむ"),
|
||||||
|
// --- Preferences Navigation & Design ---
|
||||||
|
("Pan, zoom, orbit", "パン、ズーム、オービット"),
|
||||||
|
("Reverse zoom direction", "ズーム方向を反転"),
|
||||||
|
("Zoom to the cursor", "カーソル位置へズーム"),
|
||||||
|
("Orbit around", "回転の中心"),
|
||||||
|
("View centre", "ビュー中心"),
|
||||||
|
("Point under the cursor", "カーソル下の点"),
|
||||||
|
("Animate view changes", "視点変更をアニメーション表示"),
|
||||||
|
("Animation speed", "アニメーション速度"),
|
||||||
|
("Default modelling orientation", "デフォルトのモデル方向"),
|
||||||
|
("Offer to stand up Y-up files", "Y-upファイルを立ち上げるか確認"),
|
||||||
|
("Visual style", "表示スタイル"),
|
||||||
|
// --- Application bar & Menus ---
|
||||||
|
("File", "ファイル"),
|
||||||
|
("Edit", "編集"),
|
||||||
|
("Create", "作成"),
|
||||||
|
("Modify", "修正"),
|
||||||
|
("Assemble", "アセンブリ"),
|
||||||
|
("Construct", "構築"),
|
||||||
|
("Inspect", "検査"),
|
||||||
|
("Insert", "挿入"),
|
||||||
|
("Select", "選択"),
|
||||||
|
("New Design", "新規デザイン"),
|
||||||
|
("Open...", "開く..."),
|
||||||
|
("Open", "開く"),
|
||||||
|
("Save", "保存"),
|
||||||
|
("Save As...", "名前を付けて保存..."),
|
||||||
|
("Export...", "エクスポート..."),
|
||||||
|
("Undo", "元に戻す"),
|
||||||
|
("Redo", "やり直す"),
|
||||||
|
("Cut", "切り取り"),
|
||||||
|
("Copy", "コピー"),
|
||||||
|
("Paste", "貼り付け"),
|
||||||
|
("Clear Recent", "最近使った項目をクリア"),
|
||||||
|
("Open Recent", "最近使った項目を開く"),
|
||||||
|
("Quit", "終了"),
|
||||||
|
// --- Toolbar tabs ---
|
||||||
|
("SOLID", "ソリッド"),
|
||||||
|
("SURFACE", "サーフェス"),
|
||||||
|
("MESH", "メッシュ"),
|
||||||
|
("SHEET METAL", "板金"),
|
||||||
|
("PLASTIC", "プラスチック"),
|
||||||
|
("SKETCH", "スケッチ"),
|
||||||
|
("CREATE", "作成"),
|
||||||
|
("MODIFY", "修正"),
|
||||||
|
("ASSEMBLE", "アセンブリ"),
|
||||||
|
("CONSTRUCT", "構築"),
|
||||||
|
("INSPECT", "検査"),
|
||||||
|
("INSERT", "挿入"),
|
||||||
|
("SELECT", "選択"),
|
||||||
|
("FINISH SKETCH", "スケッチを終了"),
|
||||||
|
("Finish Sketch", "スケッチを終了"),
|
||||||
|
// --- Sketch Palette ---
|
||||||
|
("SKETCH PALETTE", "スケッチ パレット"),
|
||||||
|
("Options", "オプション"),
|
||||||
|
("Linetype", "線種"),
|
||||||
|
("Normal", "標準"),
|
||||||
|
("Construction", "構築"),
|
||||||
|
("Centerline", "中心線"),
|
||||||
|
("Look At", "視点を合わせる"),
|
||||||
|
("Sketch Grid", "スケッチ グリッド"),
|
||||||
|
("Snap", "スナップ"),
|
||||||
|
("Slice", "スライス"),
|
||||||
|
("Show Profile", "プロファイルを表示"),
|
||||||
|
("Show Points", "点を表示"),
|
||||||
|
("Show Dimensions", "寸法を表示"),
|
||||||
|
("Show Constraints", "拘束を表示"),
|
||||||
|
("Show Projected Geometries", "投影ジオメトリを表示"),
|
||||||
|
("3D Sketch", "3D スケッチ"),
|
||||||
|
("Fully constrained", "完全拘束"),
|
||||||
|
("1 degree of freedom", "自由度 1"),
|
||||||
|
("degrees of freedom", "自由度"),
|
||||||
|
("Constraints conflict", "拘束が競合しています"),
|
||||||
|
// --- Dialog Titles (All Caps & Title Case) ---
|
||||||
|
("CREATE SKETCH", "スケッチを作成"),
|
||||||
|
("Create Sketch", "スケッチを作成"),
|
||||||
|
("EDIT", "編集"),
|
||||||
|
("EXTRUDE", "押し出し"),
|
||||||
|
("Extrude", "押し出し"),
|
||||||
|
("REVOLVE", "回転"),
|
||||||
|
("Revolve", "回転"),
|
||||||
|
("SWEEP", "スイープ"),
|
||||||
|
("Sweep", "スイープ"),
|
||||||
|
("LOFT", "ロフト"),
|
||||||
|
("Loft", "ロフト"),
|
||||||
|
("FILLET", "フィレット"),
|
||||||
|
("Fillet", "フィレット"),
|
||||||
|
("CHAMFER", "面取り"),
|
||||||
|
("Chamfer", "面取り"),
|
||||||
|
("SHELL", "シェル"),
|
||||||
|
("Shell", "シェル"),
|
||||||
|
("DRAFT", "勾配"),
|
||||||
|
("Draft", "勾配"),
|
||||||
|
("SCALE", "尺度"),
|
||||||
|
("Scale", "尺度"),
|
||||||
|
("COMBINE", "結合"),
|
||||||
|
("Combine", "結合"),
|
||||||
|
("SPLIT BODY", "ボディを分割"),
|
||||||
|
("Split Body", "ボディを分割"),
|
||||||
|
("MIRROR", "ミラー"),
|
||||||
|
("Mirror", "ミラー"),
|
||||||
|
("HOLE", "穴"),
|
||||||
|
("Hole", "穴"),
|
||||||
|
("THREAD", "ねじ"),
|
||||||
|
("Thread", "ねじ"),
|
||||||
|
("BOX", "直方体"),
|
||||||
|
("Box", "直方体"),
|
||||||
|
("CYLINDER", "円柱"),
|
||||||
|
("Cylinder", "円柱"),
|
||||||
|
("SPHERE", "球"),
|
||||||
|
("Sphere", "球"),
|
||||||
|
("TORUS", "トーラス"),
|
||||||
|
("Torus", "トーラス"),
|
||||||
|
("COIL", "コイル"),
|
||||||
|
("Coil", "コイル"),
|
||||||
|
("PIPE", "パイプ"),
|
||||||
|
("Pipe", "パイプ"),
|
||||||
|
("OFFSET PLANE", "オフセット平面"),
|
||||||
|
("Offset Plane", "オフセット平面"),
|
||||||
|
("PLANE AT ANGLE", "傾斜平面"),
|
||||||
|
("Plane at Angle", "傾斜平面"),
|
||||||
|
("MEASURE", "計測"),
|
||||||
|
("Measure", "計測"),
|
||||||
|
("SECTION ANALYSIS", "断面解析"),
|
||||||
|
("Section Analysis", "断面解析"),
|
||||||
|
("PARAMETERS", "パラメータ"),
|
||||||
|
("Parameters", "パラメータ"),
|
||||||
|
("MOVE", "移動"),
|
||||||
|
("Move", "移動"),
|
||||||
|
("RECTANGULAR PATTERN", "矩形パターン"),
|
||||||
|
("Rectangular Pattern", "矩形パターン"),
|
||||||
|
("CIRCULAR PATTERN", "円形パターン"),
|
||||||
|
("Circular Pattern", "円形パターン"),
|
||||||
|
("PATTERN ON PATH", "パス上のパターン"),
|
||||||
|
("Pattern on Path", "パス上のパターン"),
|
||||||
|
// --- Dialog Row Labels and Inputs ---
|
||||||
|
("Plane", "平面"),
|
||||||
|
("Profile", "プロファイル"),
|
||||||
|
("Profiles", "プロファイル"),
|
||||||
|
("Axis", "軸"),
|
||||||
|
("Axes", "軸"),
|
||||||
|
("Path", "パス"),
|
||||||
|
("Points", "点"),
|
||||||
|
("Objects", "オブジェクト"),
|
||||||
|
("Bodies", "ボディ"),
|
||||||
|
("Features", "フィーチャー"),
|
||||||
|
("Start", "開始"),
|
||||||
|
("Offset", "オフセット"),
|
||||||
|
("Profile Plane", "プロファイル平面"),
|
||||||
|
("Direction", "方向"),
|
||||||
|
("One Side", "片側"),
|
||||||
|
("Two Sides", "両側"),
|
||||||
|
("Symmetric", "対称"),
|
||||||
|
("Extent Type", "範囲タイプ"),
|
||||||
|
("Distance", "距離"),
|
||||||
|
("Distance 1", "距離 1"),
|
||||||
|
("Distance 2", "距離 2"),
|
||||||
|
("To Object", "オブジェクトまで"),
|
||||||
|
("All", "すべて"),
|
||||||
|
("Taper Angle", "テーパ角度"),
|
||||||
|
("Operation", "操作"),
|
||||||
|
("New Body", "新規ボディ"),
|
||||||
|
("Join", "結合"),
|
||||||
|
("Cut", "切り取り"),
|
||||||
|
("Intersect", "交差"),
|
||||||
|
("Angle", "角度"),
|
||||||
|
("Radius", "半径"),
|
||||||
|
("Radius Type", "半径タイプ"),
|
||||||
|
("Constant", "一定"),
|
||||||
|
("Chord Length", "弦長"),
|
||||||
|
("Variable", "可変"),
|
||||||
|
("Start Radius", "開始半径"),
|
||||||
|
("End Radius", "終了半径"),
|
||||||
|
("Flip", "反転"),
|
||||||
|
("Thickness", "厚さ"),
|
||||||
|
("Placement", "配置"),
|
||||||
|
("Single", "単一"),
|
||||||
|
("Multiple", "複数"),
|
||||||
|
("Hole Type", "穴タイプ"),
|
||||||
|
("Simple", "単純"),
|
||||||
|
("Counterbore", "座ぐり"),
|
||||||
|
("Countersink", "皿もみ"),
|
||||||
|
("Hole Tap Type", "タップタイプ"),
|
||||||
|
("Tapped", "タップ立て"),
|
||||||
|
("Drill Point", "先端"),
|
||||||
|
("Flat", "フラット"),
|
||||||
|
("Tip Angle", "先端角度"),
|
||||||
|
("Diameter", "直径"),
|
||||||
|
("Depth", "深さ"),
|
||||||
|
("Object Type", "オブジェクトタイプ"),
|
||||||
|
("Clear the selection", "選択をクリア"),
|
||||||
|
// --- Browser Tree ---
|
||||||
|
("Document Settings", "ドキュメントの設定"),
|
||||||
|
("Units", "単位"),
|
||||||
|
("Named Views", "名前付きビュー"),
|
||||||
|
("Origin", "原点"),
|
||||||
|
("Top", "上面"),
|
||||||
|
("Front", "正面"),
|
||||||
|
("Right", "右側面"),
|
||||||
|
("Home", "ホーム"),
|
||||||
|
// --- Sketch commands & tools ---
|
||||||
|
("Line", "線分"),
|
||||||
|
("Rectangle", "長方形"),
|
||||||
|
("Circle", "円"),
|
||||||
|
("Arc", "円弧"),
|
||||||
|
("Polygon", "ポリゴン"),
|
||||||
|
("Ellipse", "楕円"),
|
||||||
|
("Slot", "スロット"),
|
||||||
|
("Spline", "スプライン"),
|
||||||
|
("Conic Curve", "円錐曲線"),
|
||||||
|
("Point", "点"),
|
||||||
|
("Text", "テキスト"),
|
||||||
|
("Trim", "トリム"),
|
||||||
|
("Extend", "延長"),
|
||||||
|
("Break", "分割"),
|
||||||
|
("Sketch Scale", "スケッチ尺度"),
|
||||||
|
("Project", "投影"),
|
||||||
|
("Dimension", "寸法"),
|
||||||
|
("Sketch Dimension", "スケッチ寸法"),
|
||||||
|
("Horizontal/Vertical", "水平/垂直"),
|
||||||
|
("Coincident", "一致"),
|
||||||
|
("Tangent", "正接"),
|
||||||
|
("Equal", "等しい"),
|
||||||
|
("Parallel", "平行"),
|
||||||
|
("Perpendicular", "直交"),
|
||||||
|
("Fix/Unfix", "固定/固定解除"),
|
||||||
|
("Midpoint", "中点"),
|
||||||
|
("Concentric", "同心円"),
|
||||||
|
("Collinear", "同一直線"),
|
||||||
|
("Symmetry", "対称"),
|
||||||
|
("Curvature", "曲率"),
|
||||||
|
("Smooth", "スムーズ"),
|
||||||
|
// --- Workspace selector & environments ---
|
||||||
|
("DESIGN", "デザイン"),
|
||||||
|
("workspace", "ワークスペース"),
|
||||||
|
("Change workspace", "ワークスペースの変更"),
|
||||||
|
("Generative Design", "ジェネレーティブ デザイン"),
|
||||||
|
("Render", "レンダリング"),
|
||||||
|
("Animation", "アニメーション"),
|
||||||
|
("Simulation", "シミュレーション"),
|
||||||
|
("Manufacture", "製造"),
|
||||||
|
("Drawing", "図面"),
|
||||||
|
("coming later", "準備中"),
|
||||||
|
("current", "現在"),
|
||||||
|
// --- Browser Tree ---
|
||||||
|
("Document Settings", "ドキュメントの設定"),
|
||||||
|
("Units", "単位"),
|
||||||
|
("Named Views", "名前付きビュー"),
|
||||||
|
("Origin", "原点"),
|
||||||
|
("Top", "上面"),
|
||||||
|
("Front", "正面"),
|
||||||
|
("Right", "右側面"),
|
||||||
|
("Home", "ホーム"),
|
||||||
|
("TOP", "上面"),
|
||||||
|
("FRONT", "正面"),
|
||||||
|
("RIGHT", "右側面"),
|
||||||
|
("HOME", "ホーム"),
|
||||||
|
("Bodies", "ボディ"),
|
||||||
|
("Sketches", "スケッチ"),
|
||||||
|
("Canvases", "キャンバス"),
|
||||||
|
("Joints", "ジョイント"),
|
||||||
|
("Construction", "構築"),
|
||||||
|
("Collapse", "折りたたむ"),
|
||||||
|
("Expand", "展開"),
|
||||||
|
("Hide", "非表示"),
|
||||||
|
("Show", "表示"),
|
||||||
|
("Active component", "アクティブなコンポーネント"),
|
||||||
|
("Activate", "アクティブ化"),
|
||||||
|
];
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static CURRENT_LANG: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the current UI language for the thread.
|
||||||
|
pub fn set_current_lang(lang: &str) {
|
||||||
|
CURRENT_LANG.with(|l| {
|
||||||
|
*l.borrow_mut() = lang.to_string();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current UI language for the thread.
|
||||||
|
pub fn current_lang() -> String {
|
||||||
|
CURRENT_LANG.with(|l| l.borrow().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate text using the thread's current language.
|
||||||
|
pub fn tr(text: &str) -> &str {
|
||||||
|
CURRENT_LANG.with(|l| {
|
||||||
|
let lang = l.borrow();
|
||||||
|
if lang.as_str() == "ja" { tr_for_lang("ja", text) } else { text }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate text using a specified language.
|
||||||
|
pub fn tr_for_lang<'a>(lang: &str, text: &'a str) -> &'a str {
|
||||||
|
if lang == "ja" {
|
||||||
|
for &(en, ja) in JA_TRANSLATIONS {
|
||||||
|
if en == text {
|
||||||
|
return ja;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dynamically translate labels including patterned names like "Sketch1", "Body2", "Units: mm", etc.
|
||||||
|
pub fn tr_dyn(text: &str) -> String {
|
||||||
|
CURRENT_LANG.with(|l| {
|
||||||
|
let lang = l.borrow();
|
||||||
|
if lang.as_str() != "ja" {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
for &(en, ja) in JA_TRANSLATIONS {
|
||||||
|
if en == text {
|
||||||
|
return ja.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix("Sketch")
|
||||||
|
&& !rest.is_empty()
|
||||||
|
&& rest.chars().all(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
return format!("スケッチ{rest}");
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix("Body")
|
||||||
|
&& !rest.is_empty()
|
||||||
|
&& rest.chars().all(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
return format!("ボディ{rest}");
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix("Component")
|
||||||
|
&& !rest.is_empty()
|
||||||
|
&& rest.chars().all(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
return format!("コンポーネント{rest}");
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix("Units: ") {
|
||||||
|
return format!("単位: {rest}");
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix("Precision: ") {
|
||||||
|
return format!("精度: {rest}");
|
||||||
|
}
|
||||||
|
if let Some(rest) = text.strip_prefix("Parameters (") {
|
||||||
|
return format!("パラメータ ({rest}");
|
||||||
|
}
|
||||||
|
text.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translates_japanese() {
|
||||||
|
set_current_lang("ja");
|
||||||
|
assert_eq!(tr("Preferences"), "環境設定");
|
||||||
|
assert_eq!(tr("Extrude"), "押し出し");
|
||||||
|
assert_eq!(tr("CREATE SKETCH"), "スケッチを作成");
|
||||||
|
assert_eq!(tr("SKETCH PALETTE"), "スケッチ パレット");
|
||||||
|
assert_eq!(tr("Finish Sketch"), "スケッチを終了");
|
||||||
|
assert_eq!(tr("FINISH SKETCH"), "スケッチを終了");
|
||||||
|
assert_eq!(tr("Generative Design"), "ジェネレーティブ デザイン");
|
||||||
|
assert_eq!(tr("coming later"), "準備中");
|
||||||
|
assert_eq!(tr("Sketches"), "スケッチ");
|
||||||
|
assert_eq!(tr("NonExistentKey"), "NonExistentKey");
|
||||||
|
|
||||||
|
assert_eq!(tr_dyn("Sketch1"), "スケッチ1");
|
||||||
|
assert_eq!(tr_dyn("Body2"), "ボディ2");
|
||||||
|
assert_eq!(tr_dyn("Units: mm"), "単位: mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_english() {
|
||||||
|
set_current_lang("en");
|
||||||
|
assert_eq!(tr("Preferences"), "Preferences");
|
||||||
|
assert_eq!(tr("Extrude"), "Extrude");
|
||||||
|
assert_eq!(tr_dyn("Sketch1"), "Sketch1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,11 +27,13 @@ pub mod dialogs_surface;
|
|||||||
pub mod dim_view;
|
pub mod dim_view;
|
||||||
pub mod documents;
|
pub mod documents;
|
||||||
pub mod drag_snap;
|
pub mod drag_snap;
|
||||||
|
pub mod fonts;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
pub mod gizmo;
|
pub mod gizmo;
|
||||||
pub mod gpu;
|
pub mod gpu;
|
||||||
pub mod help;
|
pub mod help;
|
||||||
pub mod home;
|
pub mod home;
|
||||||
|
pub mod i18n;
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod inference;
|
pub mod inference;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
@@ -288,6 +290,11 @@ impl SolveApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Translate text using the active language preference.
|
||||||
|
pub fn tr<'a>(&self, text: &'a str) -> &'a str {
|
||||||
|
i18n::tr_for_lang(&self.preferences.language, text)
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw the viewport on the GPU (eframe's wgpu render state, with the app's depth and
|
/// Draw the viewport on the GPU (eframe's wgpu render state, with the app's depth and
|
||||||
/// stencil buffer bits and MSAA sample count). Without it the viewport renders on the CPU.
|
/// stencil buffer bits and MSAA sample count). Without it the viewport renders on the CPU.
|
||||||
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) {
|
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) {
|
||||||
@@ -813,6 +820,7 @@ impl SolveApp {
|
|||||||
/// Lay out the whole window.
|
/// Lay out the whole window.
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
let t0 = now_ms();
|
let t0 = now_ms();
|
||||||
|
crate::i18n::set_current_lang(&self.preferences.language);
|
||||||
scenario::clear_handles();
|
scenario::clear_handles();
|
||||||
toolbar::app_bar(self, ui);
|
toolbar::app_bar(self, ui);
|
||||||
file_load::show(self, ui.ctx());
|
file_load::show(self, ui.ctx());
|
||||||
|
|||||||
+56
-17
@@ -46,6 +46,8 @@ pub struct Prefs {
|
|||||||
pub angle_decimals: u8,
|
pub angle_decimals: u8,
|
||||||
/// `dark`, `light` or `system` (follow the operating system).
|
/// `dark`, `light` or `system` (follow the operating system).
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
/// Interface language (`en`, `ja`).
|
||||||
|
pub language: String,
|
||||||
/// Interface scale (1 = 100 %).
|
/// Interface scale (1 = 100 %).
|
||||||
pub ui_scale: f32,
|
pub ui_scale: f32,
|
||||||
/// The wheel zooms toward the point under the cursor (else toward the view's centre).
|
/// The wheel zooms toward the point under the cursor (else toward the view's centre).
|
||||||
@@ -70,6 +72,7 @@ impl Default for Prefs {
|
|||||||
length_decimals: 2,
|
length_decimals: 2,
|
||||||
angle_decimals: 1,
|
angle_decimals: 1,
|
||||||
theme: "dark".into(),
|
theme: "dark".into(),
|
||||||
|
language: "en".into(),
|
||||||
ui_scale: 1.0,
|
ui_scale: 1.0,
|
||||||
zoom_to_cursor: true,
|
zoom_to_cursor: true,
|
||||||
look_at_anim: true,
|
look_at_anim: true,
|
||||||
@@ -157,6 +160,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
|||||||
match page {
|
match page {
|
||||||
0 => {
|
0 => {
|
||||||
p.theme = d.theme;
|
p.theme = d.theme;
|
||||||
|
p.language = d.language;
|
||||||
p.ui_scale = d.ui_scale;
|
p.ui_scale = d.ui_scale;
|
||||||
p.default_units = d.default_units;
|
p.default_units = d.default_units;
|
||||||
p.length_decimals = d.length_decimals;
|
p.length_decimals = d.length_decimals;
|
||||||
@@ -376,7 +380,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let mut open = true;
|
let mut open = true;
|
||||||
let mut button: Option<&str> = None;
|
let mut button: Option<&str> = None;
|
||||||
let before = app.preferences.clone();
|
let before = app.preferences.clone();
|
||||||
crate::frame::window(ctx, "Preferences", crate::frame::Width::Wide)
|
crate::frame::window(ctx, app.tr("Preferences"), crate::frame::Width::Wide)
|
||||||
.id(egui::Id::new("sc_prefs"))
|
.id(egui::Id::new("sc_prefs"))
|
||||||
.open(&mut open)
|
.open(&mut open)
|
||||||
.collapsible(false)
|
.collapsible(false)
|
||||||
@@ -389,7 +393,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
ui.set_width(140.0);
|
ui.set_width(140.0);
|
||||||
for (i, s) in PAGES.iter().enumerate() {
|
for (i, s) in PAGES.iter().enumerate() {
|
||||||
if ui.selectable_label(app.prefs_window.section == i, *s).clicked() {
|
if ui.selectable_label(app.prefs_window.section == i, app.tr(s)).clicked() {
|
||||||
app.prefs_window.section = i;
|
app.prefs_window.section = i;
|
||||||
app.keymap.read_only = false;
|
app.keymap.read_only = false;
|
||||||
}
|
}
|
||||||
@@ -399,24 +403,24 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
// A fixed width, so the window keeps its size from page to page.
|
// A fixed width, so the window keeps its size from page to page.
|
||||||
ui.set_width(540.0);
|
ui.set_width(540.0);
|
||||||
ui.label(RichText::new(PAGES.get(app.prefs_window.section).copied().unwrap_or_default()).strong().size(14.0));
|
ui.label(RichText::new(app.tr(PAGES.get(app.prefs_window.section).copied().unwrap_or_default())).strong().size(14.0));
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
page(app, ui, &t);
|
page(app, ui, &t);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Restore Defaults").on_hover_text("Reset the settings on this page").clicked() {
|
if ui.button(app.tr("Restore Defaults")).on_hover_text(app.tr("Reset the settings on this page")).clicked() {
|
||||||
button = Some("defaults");
|
button = Some("defaults");
|
||||||
}
|
}
|
||||||
crate::frame::right_aligned(ui, 700.0, 170.0, |ui| {
|
crate::frame::right_aligned(ui, 700.0, 170.0, |ui| {
|
||||||
if ui.button("OK").clicked() {
|
if ui.button(app.tr("OK")).clicked() {
|
||||||
button = Some("ok");
|
button = Some("ok");
|
||||||
}
|
}
|
||||||
if ui.button("Cancel").clicked() {
|
if ui.button(app.tr("Cancel")).clicked() {
|
||||||
button = Some("cancel");
|
button = Some("cancel");
|
||||||
}
|
}
|
||||||
if ui.button("Apply").clicked() {
|
if ui.button(app.tr("Apply")).clicked() {
|
||||||
button = Some("apply");
|
button = Some("apply");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -478,10 +482,19 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||||||
}
|
}
|
||||||
egui::Grid::new(("sc_prefs_grid", app.prefs_window.section)).num_columns(2).spacing([16.0, 8.0]).show(ui, |ui| match app.prefs_window.section {
|
egui::Grid::new(("sc_prefs_grid", app.prefs_window.section)).num_columns(2).spacing([16.0, 8.0]).show(ui, |ui| match app.prefs_window.section {
|
||||||
0 => {
|
0 => {
|
||||||
ui.label("Theme");
|
ui.label(app.tr("Language"));
|
||||||
|
egui::ComboBox::from_id_salt("pref_language")
|
||||||
|
.selected_text(crate::i18n::LANGUAGES.iter().find(|(id, _)| *id == app.preferences.language).map_or("English", |x| x.1))
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (id, label) in crate::i18n::LANGUAGES {
|
||||||
|
ui.selectable_value(&mut app.preferences.language, id.to_string(), label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
ui.label(app.tr("Theme"));
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for (id, label) in THEMES {
|
for (id, label) in THEMES {
|
||||||
if ui.radio(app.preferences.theme == id, label).clicked() {
|
if ui.radio(app.preferences.theme == id, app.tr(label)).clicked() {
|
||||||
app.preferences.theme = id.to_string();
|
app.preferences.theme = id.to_string();
|
||||||
if id != "system" {
|
if id != "system" {
|
||||||
app.ui.dark = id == "dark";
|
app.ui.dark = id == "dark";
|
||||||
@@ -490,33 +503,34 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("UI scale");
|
ui.label(app.tr("UI scale"));
|
||||||
ui.add(egui::Slider::new(&mut app.preferences.ui_scale, 0.75..=2.0).step_by(0.05).custom_formatter(|v, _| format!("{:.0} %", v * 100.0)));
|
ui.add(egui::Slider::new(&mut app.preferences.ui_scale, 0.75..=2.0).step_by(0.05).custom_formatter(|v, _| format!("{:.0} %", v * 100.0)));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Units of new designs");
|
ui.label(app.tr("Units of new designs"));
|
||||||
egui::ComboBox::from_id_salt("pref_units").selected_text(app.preferences.default_units.clone()).show_ui(ui, |ui| {
|
egui::ComboBox::from_id_salt("pref_units").selected_text(app.preferences.default_units.clone()).show_ui(ui, |ui| {
|
||||||
for u in UNITS {
|
for u in UNITS {
|
||||||
ui.selectable_value(&mut app.preferences.default_units, u.to_string(), u);
|
ui.selectable_value(&mut app.preferences.default_units, u.to_string(), u);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Autosave every");
|
ui.label(app.tr("Autosave every"));
|
||||||
if ui.add(egui::DragValue::new(&mut app.autosave_minutes).range(0.0..=120.0).speed(0.5).suffix(" min")).changed() {
|
if ui.add(egui::DragValue::new(&mut app.autosave_minutes).range(0.0..=120.0).speed(0.5).suffix(" min")).changed() {
|
||||||
set_autosave(app);
|
set_autosave(app);
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
note(ui, "0 turns it off");
|
note(ui, "0 turns it off");
|
||||||
ui.label("Length decimals");
|
ui.label(app.tr("Length decimals"));
|
||||||
ui.add(egui::DragValue::new(&mut app.preferences.length_decimals).range(0..=6));
|
ui.add(egui::DragValue::new(&mut app.preferences.length_decimals).range(0..=6));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Angle decimals");
|
ui.label(app.tr("Angle decimals"));
|
||||||
ui.add(egui::DragValue::new(&mut app.preferences.angle_decimals).range(0..=6));
|
ui.add(egui::DragValue::new(&mut app.preferences.angle_decimals).range(0..=6));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Agent cursor");
|
ui.label(app.tr("Agent cursor"));
|
||||||
|
let speed_label = app.tr("Agent cursor speed");
|
||||||
let s = &mut app.ui.agent_cursor;
|
let s = &mut app.ui.agent_cursor;
|
||||||
ui.checkbox(&mut s.show, "Show the pointer an AI agent moves (#35)");
|
ui.checkbox(&mut s.show, "Show the pointer an AI agent moves (#35)");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Agent cursor speed");
|
ui.label(speed_label);
|
||||||
ui.add_enabled_ui(s.show, |ui| {
|
ui.add_enabled_ui(s.show, |ui| {
|
||||||
crate::agent_cursor::speed_slider(ui, &mut s.speed);
|
crate::agent_cursor::speed_slider(ui, &mut s.speed);
|
||||||
});
|
});
|
||||||
@@ -693,6 +707,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
|||||||
if !THEMES.iter().any(|(id, _)| *id == new.theme) || !["z", "y"].contains(&new.up_axis.as_str()) {
|
if !THEMES.iter().any(|(id, _)| *id == new.theme) || !["z", "y"].contains(&new.up_axis.as_str()) {
|
||||||
return Err("`theme` must be dark, light or system and `up_axis` z or y".into());
|
return Err("`theme` must be dark, light or system and `up_axis` z or y".into());
|
||||||
}
|
}
|
||||||
|
if !crate::i18n::LANGUAGES.iter().any(|(id, _)| *id == new.language) {
|
||||||
|
return Err("`language` must be en or ja".into());
|
||||||
|
}
|
||||||
if !(0.5..=2.5).contains(&new.ui_scale) || !(0.25..=4.0).contains(&new.look_at_speed) {
|
if !(0.5..=2.5).contains(&new.ui_scale) || !(0.25..=4.0).contains(&new.look_at_speed) {
|
||||||
return Err("`ui_scale` must be 0.5…2.5 and `look_at_speed` 0.25…4".into());
|
return Err("`ui_scale` must be 0.5…2.5 and `look_at_speed` 0.25…4".into());
|
||||||
}
|
}
|
||||||
@@ -778,11 +795,33 @@ mod tests {
|
|||||||
// File and Help › Keyboard Shortcuts open the same dialog at its page.
|
// File and Help › Keyboard Shortcuts open the same dialog at its page.
|
||||||
app.keymap.open = true;
|
app.keymap.open = true;
|
||||||
let ctx = egui::Context::default();
|
let ctx = egui::Context::default();
|
||||||
let _ = ctx.run_ui(Default::default(), |ui| show(&mut app, ui.ctx()));
|
let mut out = ctx.run_ui(Default::default(), |ui| show(&mut app, ui.ctx()));
|
||||||
|
out.textures_delta.clear();
|
||||||
assert!(app.prefs_window.open && app.prefs_window.section == PAGE_SHORTCUTS && !app.keymap.open);
|
assert!(app.prefs_window.open && app.prefs_window.section == PAGE_SHORTCUTS && !app.keymap.open);
|
||||||
assert!(control(&mut app, &json!({"open": "Plugins"})).is_err());
|
assert!(control(&mut app, &json!({"open": "Plugins"})).is_err());
|
||||||
assert!(set(&mut app, &json!({"up_axis": "x"})).is_err());
|
assert!(set(&mut app, &json!({"up_axis": "x"})).is_err());
|
||||||
assert!(set(&mut app, &json!({"theme": "sepia"})).is_err());
|
assert!(set(&mut app, &json!({"theme": "sepia"})).is_err());
|
||||||
|
assert!(set(&mut app, &json!({"language": "fr"})).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn language_preference_switches_and_persists() {
|
||||||
|
let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
|
||||||
|
assert_eq!(app.preferences.language, "en");
|
||||||
|
assert_eq!(app.tr("Preferences"), "Preferences");
|
||||||
|
assert_eq!(app.tr("Extrude"), "Extrude");
|
||||||
|
|
||||||
|
// Switch language to Japanese
|
||||||
|
set(&mut app, &json!({"language": "ja"})).unwrap();
|
||||||
|
assert_eq!(app.preferences.language, "ja");
|
||||||
|
assert_eq!(app.tr("Preferences"), "環境設定");
|
||||||
|
assert_eq!(app.tr("Extrude"), "押し出し");
|
||||||
|
|
||||||
|
// Preferences round-trip
|
||||||
|
let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default());
|
||||||
|
other.load_prefs(&app.prefs());
|
||||||
|
assert_eq!(other.preferences.language, "ja");
|
||||||
|
assert_eq!(other.tr("Preferences"), "環境設定");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A STEP file opened Z-up is offered to be stood up: a quarter turn about X, undoable.
|
/// A STEP file opened Z-up is offered to be stood up: a quarter turn about X, undoable.
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let mut finish = false;
|
let mut finish = false;
|
||||||
let has_curves = !selected_curves(app).is_empty();
|
let has_curves = !selected_curves(app).is_empty();
|
||||||
crate::frame::docked(
|
crate::frame::docked(
|
||||||
"SKETCH PALETTE",
|
app.tr("SKETCH PALETTE"),
|
||||||
egui::Id::new("sc_sketch_palette"),
|
egui::Id::new("sc_sketch_palette"),
|
||||||
anchor,
|
anchor,
|
||||||
ctx.content_rect().bottom() - anchor.y - 8.0,
|
ctx.content_rect().bottom() - anchor.y - 8.0,
|
||||||
@@ -215,39 +215,47 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
if ui.add(egui::Button::new(RichText::new(if folded { "+" } else { "−" }).size(14.0)).frame(false)).clicked() {
|
if ui.add(egui::Button::new(RichText::new(if folded { "+" } else { "−" }).size(14.0)).frame(false)).clicked() {
|
||||||
FOLDED.with(|f| f.set(!folded));
|
FOLDED.with(|f| f.set(!folded));
|
||||||
}
|
}
|
||||||
ui.label(RichText::new("SKETCH PALETTE").strong().size(12.5));
|
ui.label(RichText::new(app.tr("SKETCH PALETTE")).strong().size(12.5));
|
||||||
});
|
});
|
||||||
if FOLDED.with(Cell::get) {
|
if FOLDED.with(Cell::get) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ofold = OPTIONS_FOLDED.with(Cell::get);
|
let ofold = OPTIONS_FOLDED.with(Cell::get);
|
||||||
if ui.add(egui::Button::new(RichText::new(format!("{} Options", if ofold { "+" } else { "−" })).size(12.0)).frame(false)).clicked() {
|
if ui
|
||||||
|
.add(egui::Button::new(RichText::new(format!("{} {}", if ofold { "+" } else { "−" }, app.tr("Options"))).size(12.0)).frame(false))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
OPTIONS_FOLDED.with(|f| f.set(!ofold));
|
OPTIONS_FOLDED.with(|f| f.set(!ofold));
|
||||||
}
|
}
|
||||||
if !ofold {
|
if !ofold {
|
||||||
egui::Grid::new("sc_palette_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
egui::Grid::new("sc_palette_grid").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
||||||
ui.label("Linetype");
|
ui.label(app.tr("Linetype"));
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
// With a selection: convert it; without: the mode new curves are drawn in.
|
// With a selection: convert it; without: the mode new curves are drawn in.
|
||||||
let current = mode(app);
|
let current = mode(app);
|
||||||
for (i, (label, tip)) in [("—", "Normal"), ("- -", "Construction"), ("-·-", "Centerline")].iter().enumerate() {
|
for (i, (label, tip)) in [("—", "Normal"), ("- -", "Construction"), ("-·-", "Centerline")].iter().enumerate() {
|
||||||
let on = !has_curves && current == i as u8;
|
let on = !has_curves && current == i as u8;
|
||||||
let tip = if has_curves { format!("Make the selection {tip}") } else { format!("Draw {tip} curves") };
|
let tr_tip = app.tr(tip);
|
||||||
if ui.add(egui::Button::new(*label).selected(on).min_size(vec2(30.0, 20.0))).on_hover_text(tip).clicked() {
|
let tip_str = if has_curves {
|
||||||
|
format!("{} ({})", app.tr("Make the selection"), tr_tip)
|
||||||
|
} else {
|
||||||
|
format!("{} ({})", app.tr("Draw curves"), tr_tip)
|
||||||
|
};
|
||||||
|
if ui.add(egui::Button::new(*label).selected(on).min_size(vec2(30.0, 20.0))).on_hover_text(tip_str).clicked() {
|
||||||
linetype = Some(i as u8);
|
linetype = Some(i as u8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
ui.label("Look At");
|
ui.label(app.tr("Look At"));
|
||||||
if ui.button("Look At").clicked() {
|
if ui.button(app.tr("Look At")).clicked() {
|
||||||
look = true;
|
look = true;
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
for (k, label) in TOGGLES {
|
for (k, label) in TOGGLES {
|
||||||
// The label is part of the checkbox: clicking it toggles too.
|
// The label is part of the checkbox: clicking it toggles too.
|
||||||
let mut on = shown(k);
|
let mut on = shown(k);
|
||||||
if ui.checkbox(&mut on, label).changed() {
|
if ui.checkbox(&mut on, app.tr(label)).changed() {
|
||||||
set = Some((k.to_string(), on));
|
set = Some((k.to_string(), on));
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
@@ -255,11 +263,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.label(RichText::new(dof_line.as_str()).color(if ok { t.sketch_accent } else { t.text_dim }));
|
let tr_dof = app.tr(&dof_line);
|
||||||
|
ui.label(RichText::new(tr_dof).color(if ok { t.sketch_accent } else { t.text_dim }));
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add_space(WIDTH - 100.0);
|
ui.add_space(WIDTH - 100.0);
|
||||||
if ui.add(egui::Button::new(RichText::new("Finish Sketch").color(Color32::WHITE)).fill(t.accent)).clicked() {
|
if ui.add(egui::Button::new(RichText::new(app.tr("Finish Sketch")).color(Color32::WHITE)).fill(t.accent)).clicked() {
|
||||||
finish = true;
|
finish = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,26 +68,26 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
);
|
);
|
||||||
resp.on_hover_text(tip).clicked()
|
resp.on_hover_text(tip).clicked()
|
||||||
};
|
};
|
||||||
let file = click(ui, "folder", "File");
|
let file = click(ui, "folder", app.tr("File"));
|
||||||
if click(ui, "save", "Save (Ctrl+S)") {
|
if click(ui, "save", &format!("{} (Ctrl+S)", app.tr("Save"))) {
|
||||||
save(app);
|
save(app);
|
||||||
}
|
}
|
||||||
if click(ui, "undo", "Undo (Ctrl+Z)") {
|
if click(ui, "undo", &format!("{} (Ctrl+Z)", app.tr("Undo"))) {
|
||||||
let _ = app.run("edit.undo", json!({}));
|
let _ = app.run("edit.undo", json!({}));
|
||||||
}
|
}
|
||||||
if click(ui, "redo", "Redo (Ctrl+Y)") {
|
if click(ui, "redo", &format!("{} (Ctrl+Y)", app.tr("Redo"))) {
|
||||||
let _ = app.run("edit.redo", json!({}));
|
let _ = app.run("edit.redo", json!({}));
|
||||||
}
|
}
|
||||||
if click(ui, "home", "Start page") {
|
if click(ui, "home", app.tr("Home")) {
|
||||||
let rev = app.session.revision;
|
let rev = app.session.revision;
|
||||||
app.home.show(rev);
|
app.home.show(rev);
|
||||||
}
|
}
|
||||||
let theme_tip = if app.ui.dark { "Light theme" } else { "Dark theme" };
|
let theme_tip = if app.ui.dark { app.tr("Light") } else { app.tr("Dark") };
|
||||||
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
||||||
app.ui.dark = !app.ui.dark;
|
app.ui.dark = !app.ui.dark;
|
||||||
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
|
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
|
||||||
}
|
}
|
||||||
if click(ui, "help", "Help") {
|
if click(ui, "help", app.tr("Help")) {
|
||||||
app.help.menu = !app.help.menu;
|
app.help.menu = !app.help.menu;
|
||||||
let at = ui.ctx().input(|i| i.pointer.latest_pos()).map_or(300.0, |p| p.x);
|
let at = ui.ctx().input(|i| i.pointer.latest_pos()).map_or(300.0, |p| p.x);
|
||||||
ui.ctx().data_mut(|d| {
|
ui.ctx().data_mut(|d| {
|
||||||
@@ -118,7 +118,9 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
icons::paint(ui.painter(), gear.shrink(4.0), "settings", t.app_bar_text, t.app_bar_text, t.app_bar);
|
icons::paint(ui.painter(), gear.shrink(4.0), "settings", t.app_bar_text, t.app_bar_text, t.app_bar);
|
||||||
crate::scenario::publish_handle("titlebar:preferences", gear.center());
|
crate::scenario::publish_handle("titlebar:preferences", gear.center());
|
||||||
if gresp.on_hover_text(if cfg!(target_os = "macos") { "Preferences (Cmd+,)" } else { "Preferences (Ctrl+,)" }).clicked() {
|
let prefs_tip =
|
||||||
|
if cfg!(target_os = "macos") { format!("{} (Cmd+,)", app.tr("Preferences")) } else { format!("{} (Ctrl+,)", app.tr("Preferences")) };
|
||||||
|
if gresp.on_hover_text(prefs_tip).clicked() {
|
||||||
crate::prefs::open_at(app, 0);
|
crate::prefs::open_at(app, 0);
|
||||||
}
|
}
|
||||||
ui.painter().text(
|
ui.painter().text(
|
||||||
@@ -155,17 +157,17 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
let item = |ui: &mut egui::Ui, label: &str, key: &str| {
|
let item = |ui: &mut egui::Ui, label: &str, key: &str| {
|
||||||
ui.add(egui::Button::new(label).shortcut_text(key).min_size(vec2(180.0, 22.0))).clicked()
|
ui.add(egui::Button::new(label).shortcut_text(key).min_size(vec2(180.0, 22.0))).clicked()
|
||||||
};
|
};
|
||||||
if item(ui, "New Design", "Ctrl+N") {
|
if item(ui, app.tr("New Design"), "Ctrl+N") {
|
||||||
crate::documents::new_design(app);
|
crate::documents::new_design(app);
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
if item(ui, "Open…", "Ctrl+O") {
|
if item(ui, &format!("{}…", app.tr("Open")), "Ctrl+O") {
|
||||||
if let Some(p) = app.services.pick_open.as_ref().and_then(|f| f()) {
|
if let Some(p) = app.services.pick_open.as_ref().and_then(|f| f()) {
|
||||||
app.open_path_async(&p);
|
app.open_path_async(&p);
|
||||||
}
|
}
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
ui.menu_button("Open Recent", |ui| {
|
ui.menu_button(app.tr("Open Recent"), |ui| {
|
||||||
for path in app.home.recent.clone() {
|
for path in app.home.recent.clone() {
|
||||||
let missing = !solvecraft_engine::io::vfs::exists(&path);
|
let missing = !solvecraft_engine::io::vfs::exists(&path);
|
||||||
let label = format!("{path}{}", if missing { " (missing)" } else { "" });
|
let label = format!("{path}{}", if missing { " (missing)" } else { "" });
|
||||||
@@ -176,7 +178,7 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("Clear Recent").clicked() {
|
if ui.button(app.tr("Clear Recent")).clicked() {
|
||||||
app.home.recent.clear();
|
app.home.recent.clear();
|
||||||
app.home.opened_at.clear();
|
app.home.opened_at.clear();
|
||||||
close = true;
|
close = true;
|
||||||
@@ -191,11 +193,11 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
app.start("file.insert_mesh");
|
app.start("file.insert_mesh");
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
if item(ui, "Save", "Ctrl+S") {
|
if item(ui, app.tr("Save"), "Ctrl+S") {
|
||||||
save(app);
|
save(app);
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
if item(ui, "Save As…", "") {
|
if item(ui, &format!("{}…", app.tr("Save As")), "") {
|
||||||
save_as(app);
|
save_as(app);
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
@@ -217,16 +219,16 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
|||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if item(ui, "Preferences…", "") {
|
if item(ui, &format!("{}…", app.tr("Preferences")), "") {
|
||||||
app.prefs_window.open = true;
|
app.prefs_window.open = true;
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
if item(ui, "Keyboard Shortcuts…", "") {
|
if item(ui, &format!("{}…", app.tr("Keyboard Shortcuts")), "") {
|
||||||
app.keymap.open = true;
|
app.keymap.open = true;
|
||||||
app.keymap.read_only = false;
|
app.keymap.read_only = false;
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
if item(ui, "Quit", "") {
|
if item(ui, app.tr("Quit"), "") {
|
||||||
crate::documents::request_quit(app);
|
crate::documents::request_quit(app);
|
||||||
close = true;
|
close = true;
|
||||||
}
|
}
|
||||||
@@ -282,13 +284,13 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
painter.text(
|
painter.text(
|
||||||
pos2(ws.center().x - 5.0, ws.top() + 52.0),
|
pos2(ws.center().x - 5.0, ws.top() + 52.0),
|
||||||
Align2::CENTER_CENTER,
|
Align2::CENTER_CENTER,
|
||||||
crate::toolbar_custom::WORKSPACE,
|
app.tr(crate::toolbar_custom::WORKSPACE),
|
||||||
FontId::proportional(12.0),
|
FontId::proportional(12.0),
|
||||||
t.text,
|
t.text,
|
||||||
);
|
);
|
||||||
caret(&painter, pos2(ws.center().x + 27.0, ws.top() + 52.0), t.text);
|
caret(&painter, pos2(ws.center().x + 27.0, ws.top() + 52.0), t.text);
|
||||||
painter.text(pos2(ws.center().x, ws.top() + 69.0), Align2::CENTER_CENTER, "workspace", FontId::proportional(10.0), t.text_dim);
|
painter.text(pos2(ws.center().x, ws.top() + 69.0), Align2::CENTER_CENTER, app.tr("workspace"), FontId::proportional(10.0), t.text_dim);
|
||||||
if wresp.on_hover_text("Change workspace").clicked() {
|
if wresp.on_hover_text(app.tr("Change workspace")).clicked() {
|
||||||
crate::context_menu::open_for(app, ws.left_bottom(), crate::context_menu::Target::Workspace);
|
crate::context_menu::open_for(app, ws.left_bottom(), crate::context_menu::Target::Workspace);
|
||||||
}
|
}
|
||||||
crate::scenario::publish_rect("toolbar", r);
|
crate::scenario::publish_rect("toolbar", r);
|
||||||
@@ -300,7 +302,8 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
tabs.extend(TABS.iter());
|
tabs.extend(TABS.iter());
|
||||||
for tab in tabs {
|
for tab in tabs {
|
||||||
let w = 12.0 + tab.len() as f32 * 7.4;
|
let display_tab = app.tr(tab);
|
||||||
|
let w = 12.0 + (display_tab.len() as f32 * 7.4).max(28.0);
|
||||||
let tr = Rect::from_min_size(pos2(x, tabs_y), vec2(w, 22.0));
|
let tr = Rect::from_min_size(pos2(x, tabs_y), vec2(w, 22.0));
|
||||||
let active = app.ui.tab == tab;
|
let active = app.ui.tab == tab;
|
||||||
let resp = ui.interact(tr, ui.id().with(("tab", tab)), Sense::click());
|
let resp = ui.interact(tr, ui.id().with(("tab", tab)), Sense::click());
|
||||||
@@ -317,7 +320,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
} else {
|
} else {
|
||||||
t.text_dim
|
t.text_dim
|
||||||
};
|
};
|
||||||
painter.text(tr.center(), Align2::CENTER_CENTER, tab, FontId::proportional(12.0), col);
|
painter.text(tr.center(), Align2::CENTER_CENTER, display_tab, FontId::proportional(12.0), col);
|
||||||
if resp.clicked() {
|
if resp.clicked() {
|
||||||
app.ui.tab = tab.to_string();
|
app.ui.tab = tab.to_string();
|
||||||
}
|
}
|
||||||
@@ -384,9 +387,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
};
|
};
|
||||||
let icon = if c.id == "sketch.finish" { "finish" } else { c.icon };
|
let icon = if c.id == "sketch.finish" { "finish" } else { c.icon };
|
||||||
icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc);
|
icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc);
|
||||||
|
let tr_label = app.tr(c.label);
|
||||||
let tip = match &info.disabled_reason {
|
let tip = match &info.disabled_reason {
|
||||||
Some(why) => format!("{}\n{why}", c.label),
|
Some(why) => format!("{}\n{why}", tr_label),
|
||||||
None => format!("{}{}", c.label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()),
|
None => format!("{}{}", tr_label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()),
|
||||||
};
|
};
|
||||||
if resp.on_hover_text(tip).clicked() && info.enabled {
|
if resp.on_hover_text(tip).clicked() && info.enabled {
|
||||||
let id = c.id;
|
let id = c.id;
|
||||||
@@ -399,7 +403,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
// Panel label with drop-down.
|
// Panel label with drop-down.
|
||||||
let lr = Rect::from_min_size(pos2(px, top + 42.0), vec2(width, 18.0));
|
let lr = Rect::from_min_size(pos2(px, top + 42.0), vec2(width, 18.0));
|
||||||
let label = panel.to_string();
|
let label = app.tr(panel).to_string();
|
||||||
let lresp = ui.interact(lr, ui.id().with(("panel", panel)), Sense::click());
|
let lresp = ui.interact(lr, ui.id().with(("panel", panel)), Sense::click());
|
||||||
crate::scenario::publish_handle(&format!("panel:{tab}:{panel}"), lr.center());
|
crate::scenario::publish_handle(&format!("panel:{tab}:{panel}"), lr.center());
|
||||||
if lresp.hovered() && enabled_panel {
|
if lresp.hovered() && enabled_panel {
|
||||||
@@ -437,7 +441,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let (ir, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover());
|
let (ir, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover());
|
||||||
icons::paint(ui.painter(), ir, c.icon, t.icon, t.icon_fill, t.accent);
|
icons::paint(ui.painter(), ir, c.icon, t.icon, t.icon_fill, t.accent);
|
||||||
let b = ui.add_enabled(info.enabled, egui::Button::new(c.label).frame(false).shortcut_text(c.shortcut.unwrap_or("")));
|
let b = ui.add_enabled(
|
||||||
|
info.enabled,
|
||||||
|
egui::Button::new(app.tr(c.label)).frame(false).shortcut_text(c.shortcut.unwrap_or("")),
|
||||||
|
);
|
||||||
crate::scenario::publish_handle(&format!("toolbar:{}", c.id), b.rect.center());
|
crate::scenario::publish_handle(&format!("toolbar:{}", c.id), b.rect.center());
|
||||||
if b.secondary_clicked() {
|
if b.secondary_clicked() {
|
||||||
menu = Some((b.interact_pointer_pos().unwrap_or(b.rect.left_bottom()), c.id));
|
menu = Some((b.interact_pointer_pos().unwrap_or(b.rect.left_bottom()), c.id));
|
||||||
|
|||||||
Reference in new issue
Block a user