Compare commits

..

14 Commits

Author SHA1 Message Date
jyviko e894f4a12f i18n tests: the painted-English sweeps install the catalogs themselves (#13290)
no_english_is_painted_where_the_language_has_a_translation and
the_sweep_reports_english_only_where_a_translation_exists read translations without installing the
app's catalogs, so they passed only when another test in the same process had installed them first
(run alone, every id read as itself).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:56:31 -05:00
jyviko 7b9e66929b Shared UI crates: ui-text, the message catalogs' machinery as a shared crate (#13290)
bidi, Catalog, Locale, lookup, formatting, labels and language choice move from ui-egui's
i18n into solidcraft-ui-text, a Shared crate any front end can use. The host installs its
catalogs once; with none installed every id reads as itself. SolidCraft's 35 catalogs, the
build script that merges them, and the adapters that read engine types (diagnostic,
tr_engine_error, and the given-name and browser-label vocabulary as the LocaleNames trait)
stay in the app's i18n. The app installs its catalogs when it starts, when an app is built
and in the widget test context. Call sites use the crate's path, with no re-export at the
old one; messages, ids and fallbacks are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:56:31 -05:00
jyviko bd3a94e946 Material editor: the Remove buttons are translated (material.editor.remove)
The editor's two Remove buttons called tr("Remove"), which looks up an id, so they read
"Remove" in every language. They use a new id, material.editor.remove, whose text in each
locale is the one that locale already gives the plain Remove button
prefs.network_host_remove (33 of 35 catalogs have it; the other two show English, as now).
The label-literal guard found it once the file's import named tr directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:43 -05:00
jyviko b0d7219115 Colour: the sRGB rule and the analysis palette are a shared crate, solidcraft-color (#13291)
geom's srgb module (the one sRGB transfer function every encode and decode goes through)
moves whole into solidcraft-color with its tests, and so does AnalysisColors, the analysis
palette the theme fills and the renderer reads. render, doc, io, engine, gpu and ui-egui use
the crate; there is no re-export at the old paths. The theme's unused material_color wrapper
is removed, so no non-test theme code names solidcraft_engine. Values are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:43 -05:00
jyviko a7598a92c6 UI kit: length fields take unit scaling and length parsing from the host (#13270)
The kit's length field converted millimetres to and from the design's units through three
prefs helpers. Those are the host's unit rules, so Expressions gains unit_scale,
length_number and parse_length_mm, and length_drag reads them from the context.
DocExpressions implements them with the same prefs helpers, so the app is unchanged. With
nothing installed the field edits plain millimetres. No kit file calls prefs outside
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:43 -05:00
jyviko 5c4932ba42 UI kit: continuity rows leave the kit, the navigator figure takes its axes as input (#13261)
continuity_row and its explained and tip variants are CAD controls over the kernel's
Continuity: they move beside their callers in dialogs_surface, still drawn with the kit's
choice_row. The Preferences navigator figure takes its axes (screen direction and depth)
as NavigatorAxis input; SolidCraft computes them from its default camera. dashed_line is
painter geometry and moves into ui_kit::dash with its test. No kit file uses the engine,
the record or the viewport outside tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:43 -05:00
jyviko ee72a98948 UI kit: layout mode and card geometry in the kit, input shield set by the host (#13260)
LayoutMode (with its per-context publish and current), the card gap and the panic-free
clamp move into the kit's containers and panels, and Cards into stages. Cards::new takes
the workspace width instead of reading the ribbon's constant. The flag that says a report
covers the 3D view is now the kit's (input_blocked, set_input_blocked); the app's CadInput
guard sets it, and issue_report's own copy is gone. Serde names are unchanged, so saved
preferences load as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:21 -05:00
jyviko 2fba25f564 UI kit: navigation-bar values and the horizontal wheel in the kit (#13259)
The bar's edge, size, idle and button-kind values move beside the bar's drawing in
ui_kit::navigation as NavigationEdge, NavigationSize, NavigationIdle and NavigationKind (the
kit already has a container Edge). Serde names are unchanged, so saved preferences load as
before. take_horizontal_wheel moves into the theme picker, its only caller. SolidCraft's
slot table stays in navigation_bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
jyviko 7b90cdf3fb UI kit: number parsing and recent colours in the kit, expression support passed in by the host (#13258)
parse_number (decimal point or comma, Unicode minus) moves from prefs into the kit's
fields, and recent_colors moves whole into ui_kit::recent_colors beside the colour picker.
The kit no longer knows the expression language: a host installs an Expressions (is this
a literal, is this a unit, complete or step this text) per egui context, and fields read
it for label drags and completion. SolidCraft installs DocExpressions, built on doc::expr
and params_dialog::complete, when the app first draws into a context. Behaviour in the app
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
jyviko 8b7586ba87 UI kit: the kit owns the menu entry and the list width (#13257)
Item (a command or ui.* action with its label, shortcut, icon, params, children, swatch
and tip) and LIST_W move from context_menu into ui_kit::menu, with the same serde form, so
the control channel's menu report is byte-identical. context_menu keeps building
SolidCraft's entries (cmd, and act over Item::action) and returns kit Items. No kit file
refers to context_menu any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
jyviko b23d1ada5d UI kit: Preferences search is a kit module, settings_search (#13256)
prefs_search moves whole into ui_kit::settings_search: it filters any settings page the
kit draws, and SolidCraft's Preferences window is one use of it. It publishes its handles
into the widget registry and reads the same wall clock as before (web_time), so no kit
file calls the app for search any more. Every caller uses the new path; state ids, handle
names, matches and scores are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
jyviko 9a0d9ce243 Theme: the tab accent takes the tab, the sketch and the tool as data (#12938)
TabAccent::publish takes the open tab, whether a sketch is being edited and whether a tool
or dialog is active, instead of SolveApp; SolveApp::publish_tab_accent works the three out
in one place. scoped_panel_tokens is public, so a host can draw any component in another
palette or accent. The theme and its tests no longer name SolveApp, Services or workspace.
Accents are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
jyviko be226abee7 UI kit: the widget registry is a shared crate, ui-registry (#12936)
The handle, rect and count registry that UI tests, scenarios and the control channel read
moves out of scenario into solidcraft-ui-registry, the first Shared crate. The kit publishes
into it directly; scenario re-exports it, so scenarios, scripts and the rest of ui-egui are
unchanged and every handle keeps its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
jyviko 414b8bb139 Layers: a Shared crate class, and cargo xtask shared builds each one on its own (#12935)
A Shared crate depends only on other Shared crates, solidcore and external crates (UI
toolkits included); any layered or testkit crate may use one. The layer check reports a
Shared crate's dependency on an app crate, of any kind. cargo xtask shared checks each
Shared crate's manifest (workspace lints, licence, README) and that no include_str! or
include_bytes! reaches outside it, then builds and tests it with its Shared dependencies in
a scratch workspace of its own. cargo xtask ci runs it after the layer check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 07:49:09 -05:00
312 changed files with 5440 additions and 3782 deletions
Generated
+31 -2
View File
@@ -4097,6 +4097,7 @@ dependencies = [
"solidcraft-gpu",
"solidcraft-space-mouse",
"solidcraft-ui-egui",
"solidcraft-ui-text",
"ureq",
"windows-sys 0.61.2",
"winit",
@@ -4123,6 +4124,10 @@ dependencies = [
"solidcraft-mcp",
]
[[package]]
name = "solidcraft-color"
version = "0.40.0-rc.3"
[[package]]
name = "solidcraft-derive"
version = "0.40.0-rc.3"
@@ -4141,6 +4146,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-derive",
"solidcraft-geom",
"solidcraft-kernel",
@@ -4173,6 +4179,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"solidcore-record",
"solidcraft-color",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-io",
@@ -4207,6 +4214,7 @@ dependencies = [
"serde",
"serde_json",
"solidcore-record",
"solidcraft-color",
"solidcraft-engine",
"solidcraft-geom",
"solidcraft-io",
@@ -4234,6 +4242,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
@@ -4293,6 +4302,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-geom",
"web-time",
"weezl",
@@ -4363,20 +4373,38 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-space-mouse",
"solidcraft-ui-registry",
"solidcraft-ui-text",
"syn 2.0.119",
"sys-locale",
"tiny-skia",
"toml",
"ttf-parser",
"unicode-bidi",
"web-time",
"winreg",
"zip",
]
[[package]]
name = "solidcraft-ui-registry"
version = "0.40.0-rc.3"
dependencies = [
"egui",
]
[[package]]
name = "solidcraft-ui-text"
version = "0.40.0-rc.3"
dependencies = [
"log",
"serde_json",
"sys-locale",
"unicode-bidi",
]
[[package]]
name = "solidcraft-web"
version = "0.40.0-rc.3"
@@ -4389,6 +4417,7 @@ dependencies = [
"serde_json",
"solidcraft-engine",
"solidcraft-ui-egui",
"solidcraft-ui-text",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+3
View File
@@ -28,6 +28,9 @@ solidcraft-doc = { path = "crates/doc" }
solidcraft-io = { path = "crates/io" }
solidcraft-engine = { path = "crates/engine" }
solidcraft-ui-egui = { path = "crates/ui-egui" }
solidcraft-ui-registry = { path = "crates/ui-registry" }
solidcraft-color = { path = "crates/color" }
solidcraft-ui-text = { path = "crates/ui-text" }
solidcraft-space-mouse = { path = "crates/space-mouse" }
solidcraft-mcp = { path = "crates/mcp" }
solidcraft-derive = { path = "crates/derive" }
+1
View File
@@ -23,6 +23,7 @@ profile = ["solidcraft-engine/profile"]
[dependencies]
solidcraft-engine = { workspace = true }
solidcraft-ui-egui = { workspace = true }
solidcraft-ui-text = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
+4 -4
View File
@@ -48,15 +48,15 @@ impl Download {
}
if solidcraft_ui_egui::documents::any_dirty(app) {
let mut answer = None;
let closed = ui_kit::Window::new(egui::Id::unique("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
let closed = ui_kit::Window::new(egui::Id::unique("open_link"), "status_warning", solidcraft_ui_text::tr("web.open_link"))
.required()
.show_body(ctx, |ui| {
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
ui.label(solidcraft_ui_text::tr("web.open_link_confirm"));
ui.horizontal(|ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr("ui.open"), ui_kit::Button::Normal).clicked() {
answer = Some(true);
}
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
answer = Some(false);
}
});
+9 -8
View File
@@ -810,7 +810,7 @@ mod web {
files::pick_from_disk(&self.files, Intent::Open);
}
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
.backing_on_hover_text(solidcraft_ui_text::tr("web.files.insert_drawings_hint"))
.clicked()
{
files::pick_from_disk(&self.files, Intent::Insert);
@@ -864,7 +864,7 @@ mod web {
let design = solidcraft_engine::io::is_design_path(&e.name);
if !design
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
.backing_on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
.backing_on_hover_text(solidcraft_ui_text::tr("web.files.insert_stored_drawings_hint"))
.clicked()
{
act = Some(("insert", e.name.clone()));
@@ -1016,7 +1016,7 @@ mod web {
/// Whether the shell already has the on-demand face the live language needs.
fn script_font_ready(&self) -> bool {
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_text::effective_language(&self.app.preferences.language)) {
Some("zh") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh"),
Some("zh-TW") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh-TW"),
Some("ja") => self.cjk_bytes.is_some(),
@@ -1058,7 +1058,7 @@ mod web {
false
}
};
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
let effective = solidcraft_ui_text::effective_language(&self.app.preferences.language);
if !installed && solidcraft_ui_egui::fonts::needs_cjk_font(effective) {
log::warn!("script font unavailable; showing English for this session");
}
@@ -1067,13 +1067,13 @@ mod web {
ctx.request_repaint();
}
let ready = self.script_font_ready();
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
let effective = solidcraft_ui_text::effective_language(&self.app.preferences.language);
if self.font.tick(&mut self.app.preferences.language, ready) {
self.fetch_font(ctx, solidcraft_ui_egui::fonts::web_script_font_kind(effective).unwrap_or("ja"));
}
// The CJK face leads only a CJK UI: its Cyrillic glyphs are full-width, so switching
// to Ukrainian (or English) must put the Latin face back in front.
if solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language) != self.font_language {
if solidcraft_ui_text::effective_language(&self.app.preferences.language) != self.font_language {
self.apply_fonts(ctx);
}
}
@@ -1081,7 +1081,7 @@ mod web {
/// Set up the UI fonts for the live language, keeping fetched script fonts as fallbacks
/// (Chinese leads a Chinese UI; see [`solidcraft_ui_egui::fonts::install_web_script_fonts`]).
fn apply_fonts(&mut self, ctx: &egui::Context) {
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
let effective = solidcraft_ui_text::effective_language(&self.app.preferences.language);
if self.cjk_bytes.is_none() && self.chinese_bytes.is_none() {
solidcraft_ui_egui::fonts::setup_fonts(ctx);
} else {
@@ -1149,7 +1149,7 @@ mod web {
UNSAVED.with(|u| u.set(self.asks_before_unload()));
if self.launch.embed {
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
if ui_kit::button(ui, solidcraft_ui_text::tr("web.open_full"), ui_kit::Button::Normal).clicked()
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
{
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
@@ -1225,6 +1225,7 @@ mod web {
pub fn start() {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
solidcraft_ui_egui::i18n::install();
wasm_bindgen_futures::spawn_local(async {
let Some(window) = web_sys::window() else { return };
let launch = crate::Launch::parse(&query());
+1
View File
@@ -34,6 +34,7 @@ solidcraft-engine = { workspace = true }
# The D3D12 shader compiler the window starts with (#6756).
solidcraft-gpu = { workspace = true }
solidcraft-ui-egui = { workspace = true }
solidcraft-ui-text = { workspace = true }
solidcraft-space-mouse = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
egui = { workspace = true }
+2 -1
View File
@@ -518,7 +518,7 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
// The groups come from the format registry, so a new format joins this dialog by itself.
let mut everything = io::formats::open_extensions(true);
everything.extend(["dxf", "svg"]);
dialog = dialog.add_filter(solidcraft_ui_egui::i18n::tr("file.open.drawing_filter"), &case_variants(&everything));
dialog = dialog.add_filter(solidcraft_ui_text::tr("file.open.drawing_filter"), &case_variants(&everything));
for (label, exts) in io::formats::import_filters(true) {
dialog = dialog.add_filter(label, &case_variants(&exts));
}
@@ -945,6 +945,7 @@ fn main() -> eframe::Result {
#[cfg(target_os = "macos")]
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
mark("main");
solidcraft_ui_egui::i18n::install();
// On OCLP-patched Macs, wgpu picks a Metal Shading Language standard that the system's
// compiler refuses (`invalid value 'metal3.2'`), because the patched OS reports a newer
// macOS version than the GPU's Metal compiler supports. Re-launching with
@@ -0,0 +1,101 @@
{
"ar": {
"material.editor.remove": "إزالة"
},
"bg": {
"material.editor.remove": "Remove"
},
"bs": {
"material.editor.remove": "Ukloni"
},
"ca": {
"material.editor.remove": "Eliminar"
},
"cs": {
"material.editor.remove": "Odebrat"
},
"da": {
"material.editor.remove": "Fjern"
},
"de": {
"material.editor.remove": "Entfernen"
},
"en": {
"material.editor.remove": "Remove"
},
"es": {
"material.editor.remove": "Eliminar"
},
"fi": {
"material.editor.remove": "Poista"
},
"fr": {
"material.editor.remove": "Retirer"
},
"gl": {
"material.editor.remove": "Quitar"
},
"he": {
"material.editor.remove": "הסרה"
},
"hr": {
"material.editor.remove": "Ukloni"
},
"hu": {
"material.editor.remove": "Eltávolítás"
},
"id": {
"material.editor.remove": "Hapus"
},
"it": {
"material.editor.remove": "Rimuovi"
},
"ja": {
"material.editor.remove": "削除"
},
"ka": {
"material.editor.remove": "წაშლა"
},
"ms": {
"material.editor.remove": "Buang"
},
"nb": {
"material.editor.remove": "Fjern"
},
"nl": {
"material.editor.remove": "Verwijderen"
},
"pl": {
"material.editor.remove": "Usuń"
},
"pt-BR": {
"material.editor.remove": "Remover"
},
"ro": {
"material.editor.remove": "Elimină"
},
"ru": {
"material.editor.remove": "Убрать"
},
"sr": {
"material.editor.remove": "Уклони"
},
"sv": {
"material.editor.remove": "Ta bort"
},
"tr": {
"material.editor.remove": "Kaldır"
},
"uk": {
"material.editor.remove": "Вилучити"
},
"vi": {
"material.editor.remove": "Xóa"
},
"zh-TW": {
"material.editor.remove": "刪除"
},
"zh": {
"material.editor.remove": "删除"
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "solidcraft-color"
description = "Colour rules and data any crate may share: the sRGB transfer function and the surface analysis palette"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
+20
View File
@@ -0,0 +1,20 @@
# solidcraft-color
Colour rules and data that any crate may share. It has no dependencies and holds no app types,
so the renderer, the document, the importers and the UI theme all use the same rule and the same
colours instead of keeping copies.
The sRGB transfer function (IEC 61966-2-1) lives here once: every encode and decode in Rust goes
through `srgb`, so a change to the rule is one change. Shaders can't call Rust and keep their own
copies.
## API
| Item | What it is |
|---|---|
| `srgb::encode(linear)` | Linear light (0..1) to an sRGB value (0..1) |
| `srgb::decode(encoded)` | An sRGB value (0..1) to linear light |
| `srgb::encode8(linear)` | Linear light to the nearest 8-bit sRGB code (out of range clamps, NaN reads as 0) |
| `srgb::decode8(code)` | An 8-bit sRGB code to linear light |
| `AnalysisColors` | The surface analysis palette (draft, accessibility, curvature, hatch), straight sRGB 0..1 as the shader takes it; the theme fills it and the renderer reads it |
| `AnalysisColors::DEFAULT` | The light theme's colours with no colour vision mode, as headless pictures use |
+32
View File
@@ -0,0 +1,32 @@
//! The colours a surface analysis is shaded in.
/// Surface analysis colours, straight sRGB 0..1 as the shader takes them.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnalysisColors {
/// Draft Analysis: at least the draft angle, between, at most minus the angle.
pub draft: [[f32; 3]; 3],
/// Accessibility Analysis: reachable, blocked.
pub access: [[f32; 3]; 2],
/// Curvature Map: flat, the middle of the scale, at or below the analysis radius. Dark blue
/// to light yellow in one straight blend, so the lightness rises the whole way and the scale
/// reads with hue lost too (#975).
pub curvature: [[f32; 3]; 3],
/// Stripe the negative draft and blocked faces too, so colour is not the only cue.
pub hatch: bool,
}
impl AnalysisColors {
/// The light theme's colours (no colour vision mode): what headless pictures use.
pub const DEFAULT: AnalysisColors = AnalysisColors {
draft: [[0.20, 0.75, 0.30], [0.95, 0.80, 0.15], [0.85, 0.22, 0.20]],
access: [[0.20, 0.75, 0.30], [0.85, 0.22, 0.20]],
curvature: [[0.08, 0.12, 0.45], [0.53, 0.51, 0.425], [0.98, 0.90, 0.40]],
hatch: false,
};
}
impl Default for AnalysisColors {
fn default() -> Self {
AnalysisColors::DEFAULT
}
}
+8
View File
@@ -0,0 +1,8 @@
//! Colour rules and data shared by every crate: the sRGB transfer function ([`srgb`]) and the surface analysis palette
//! ([`AnalysisColors`]). No app types and no dependencies, so the renderer, the document and the theme all read the same
//! rule and the same colours.
mod analysis;
pub mod srgb;
pub use analysis::AnalysisColors;
+1
View File
@@ -10,6 +10,7 @@ workspace = true
[dependencies]
solidcore-record = { workspace = true }
solidcraft-color = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-sketch = { workspace = true }
solidcraft-kernel = { workspace = true }
+1 -1
View File
@@ -154,7 +154,7 @@ impl VisualPreset {
match self.recipe {
Recipe::Opaque(r) => r.color,
// A UI swatch of the transmission values, not rendered glass or absorption.
Recipe::Glass(r) => r.transmission_linear_rgb.map(solidcraft_geom::srgb::encode8),
Recipe::Glass(r) => r.transmission_linear_rgb.map(solidcraft_color::srgb::encode8),
}
}
+1
View File
@@ -36,6 +36,7 @@ required-features = ["solver-profiling"]
[dependencies]
solidcore-record = { workspace = true }
solidcraft-color = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-sketch = { workspace = true }
solidcraft-kernel = { workspace = true }
+1 -1
View File
@@ -1721,7 +1721,7 @@ fn cpu_saved_thin_sheet_uses_its_authored_ior_beer_thickness_and_roundtrips_offl
let a = (-sigma * 2.0).exp();
let denominator = 1.0 - f * f * a * a;
let scene = f + (1.0 - f).powi(2) * (f * a * a + a) / denominator;
solidcraft_geom::srgb::encode8(scene / (1.0 + scene))
solidcraft_color::srgb::encode8(scene / (1.0 + scene))
});
assert_eq!(decoded.get_pixel(0, 0).0, [rgb[0], rgb[1], rgb[2], 255]);
assert!(Arc::ptr_eq(&before, &s.doc));
+1 -1
View File
@@ -401,7 +401,7 @@ pub(super) fn apply_appearance(s: &mut Session, params: &Value) -> Result<Value>
let value = item("surfaces", &input.id, command)?;
let lo = triple(&value["base_linear_low"], command)?;
let hi = triple(&value["base_linear_high"], command)?;
let color = std::array::from_fn::<_, 3, _>(|i| crate::geom::srgb::encode8((lo[i] + hi[i]) * 0.5));
let color = std::array::from_fn::<_, 3, _>(|i| solidcraft_color::srgb::encode8((lo[i] + hi[i]) * 0.5));
(
value,
LibraryLook {
+2 -1
View File
@@ -6,8 +6,9 @@ use solidcraft_geom::prelude::*;
use std::sync::Arc;
use solidcore_record::{Plane, Vec3};
use solidcraft_color::AnalysisColors;
use solidcraft_geom::Aabb3;
use solidcraft_render::{AnalysisColors, Camera, Rgb, Scene, SceneAnalysis, SceneLine, SceneMesh};
use solidcraft_render::{Camera, Rgb, Scene, SceneAnalysis, SceneLine, SceneMesh};
use solidcraft_sketch::Sketch;
use crate::{Result, Sel, Session};
+2 -2
View File
@@ -222,7 +222,7 @@ const MARK: Palette = Palette {
cap: Rgb(55, 56, 57),
hatch: [Rgb(61, 62, 63), Rgb(64, 65, 66)],
isocurve: Rgb(58, 59, 60),
analysis: solidcraft_render::AnalysisColors {
analysis: solidcraft_color::AnalysisColors {
draft: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]],
access: [[0.11, 0.12, 0.13], [0.14, 0.15, 0.16]],
curvature: [[0.17, 0.18, 0.19], [0.21, 0.22, 0.23], [0.24, 0.25, 0.26]],
@@ -253,7 +253,7 @@ fn the_default_palette_is_the_colour_table() {
(colors::SKETCH_PROJECTED, colors::SKETCH_LOST, colors::SKETCH_CONSTRUCTION, colors::SKETCH_CENTERLINE)
);
assert_eq!((p.axes, p.cap, p.isocurve), ([colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z], colors::SECTION_CAP, colors::ISOCURVE));
assert_eq!(p.analysis, solidcraft_render::AnalysisColors::DEFAULT);
assert_eq!(p.analysis, solidcraft_color::AnalysisColors::DEFAULT);
}
#[test]
-1
View File
@@ -40,7 +40,6 @@ mod profile;
pub mod section;
pub mod simplify;
pub mod spline_space;
pub mod srgb;
pub mod stderr;
pub mod tess;
pub mod title_block;
+1
View File
@@ -11,6 +11,7 @@ workspace = true
[dependencies]
solidcore-record = { workspace = true }
robust = "1.2"
solidcraft-color = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-render = { workspace = true }
wgpu = { workspace = true }
+1 -1
View File
@@ -4528,7 +4528,7 @@ fn uniform_bytes_placing(cb: &Frame, w: f32, h: f32, linear: bool, reversed: boo
/// The shader's analysis colours (`ana_c`: draft positive, between, negative; reachable,
/// blocked; curvature flat, middle, tight; alpha 1) and flags (`ana_f.x`: 1 to stripe the bad
/// faces).
pub fn analysis_colors(c: &solidcraft_render::AnalysisColors) -> [f32; 36] {
pub fn analysis_colors(c: &solidcraft_color::AnalysisColors) -> [f32; 36] {
let mut v = [0.0; 36];
let colors = c.draft.iter().chain(&c.access).chain(&c.curvature).flat_map(|rgb| [rgb[0], rgb[1], rgb[2], 1.0]);
for (slot, x) in v.iter_mut().zip(colors) {
+2 -2
View File
@@ -301,7 +301,7 @@ fn pictures_show_the_surface_analysis() {
s.execute("inspect.curvature_map", &json!({})).unwrap();
let curvature = picture(&s);
let near = |p: [u8; 4], c: [u8; 3]| (0..3).all(|k| (i32::from(p[k]) - i32::from(c[k])).abs() < 60);
let flat = solidcraft_engine::render::AnalysisColors::DEFAULT.curvature[0].map(|v| (v * 255.0).round() as u8);
let flat = solidcraft_color::AnalysisColors::DEFAULT.curvature[0].map(|v| (v * 255.0).round() as u8);
let flats = |img: &[u8]| pixels(img).filter(|p| near(*p, flat)).count();
// The flat colour is dark, so the plain picture's edge lines come near it in places.
assert!(
@@ -317,7 +317,7 @@ fn pictures_show_the_surface_analysis() {
assert!(black > 500 && white > 500, "zebra: {black} black, {white} white pixels");
s.execute("inspect.accessibility", &json!({})).unwrap();
let access = picture(&s);
let [ok, blocked] = colors_of(solidcraft_engine::render::AnalysisColors::DEFAULT.access);
let [ok, blocked] = colors_of(solidcraft_color::AnalysisColors::DEFAULT.access);
let (reachable, under) = (pixels(&access).filter(|p| near(*p, ok)).count(), pixels(&access).filter(|p| near(*p, blocked)).count());
assert!(reachable > 1000 && under > 1000, "accessibility: {reachable} reachable, {under} blocked pixels");
}
+1
View File
@@ -14,6 +14,7 @@ trust-unknown-source = []
[dependencies]
solidcore-record = { workspace = true }
solidcraft-color = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-kernel = { workspace = true }
solidcraft-doc = { workspace = true }
+1 -1
View File
@@ -842,7 +842,7 @@ impl<'d> Reader<'d, '_, '_> {
/// A linear base colour channel as an 8-bit sRGB code; one that isn't a number reads as 0.8.
fn srgb(c: f32) -> u8 {
solidcraft_geom::srgb::encode8(if c.is_finite() { f64::from(c) } else { 0.8 })
solidcraft_color::srgb::encode8(if c.is_finite() { f64::from(c) } else { 0.8 })
}
/// A material's look: base colour, alpha when blended, metallic/roughness when opaque.
+1
View File
@@ -14,6 +14,7 @@ raytrace-cpu = ["dep:robust"]
[dependencies]
solidcore-record = { workspace = true }
solidcraft-color = { workspace = true }
solidcraft-geom = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+1 -31
View File
@@ -1,37 +1,7 @@
//! Surface analysis as a view draws it: the shading mode and the colours it uses.
use solidcore_record::Vec3;
/// Surface analysis colours, straight sRGB 0..1 as the shader takes them.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnalysisColors {
/// Draft Analysis: at least the draft angle, between, at most minus the angle.
pub draft: [[f32; 3]; 3],
/// Accessibility Analysis: reachable, blocked.
pub access: [[f32; 3]; 2],
/// Curvature Map: flat, the middle of the scale, at or below the analysis radius. Dark blue
/// to light yellow in one straight blend, so the lightness rises the whole way and the scale
/// reads with hue lost too (#975).
pub curvature: [[f32; 3]; 3],
/// Stripe the negative draft and blocked faces too, so colour is not the only cue.
pub hatch: bool,
}
impl AnalysisColors {
/// The light theme's colours (no colour vision mode): what headless pictures use.
pub const DEFAULT: AnalysisColors = AnalysisColors {
draft: [[0.20, 0.75, 0.30], [0.95, 0.80, 0.15], [0.85, 0.22, 0.20]],
access: [[0.20, 0.75, 0.30], [0.85, 0.22, 0.20]],
curvature: [[0.08, 0.12, 0.45], [0.53, 0.51, 0.425], [0.98, 0.90, 0.40]],
hatch: false,
};
}
impl Default for AnalysisColors {
fn default() -> Self {
AnalysisColors::DEFAULT
}
}
use solidcraft_color::AnalysisColors;
/// A way of shading the model's faces to judge their shape.
#[derive(Clone, Copy, Debug, PartialEq)]
+1 -1
View File
@@ -10,7 +10,7 @@ use super::{CompletePass, MAX_BYTES, invalid, limit};
use crate::job::encoding::PngEncoding;
use crate::job::{CancelToken, CpuHost, ErrorCode, JobError, Phase, Progress, Reservation, Result};
use solidcore_record::Vec3;
use solidcraft_geom::srgb::encode8;
use solidcraft_color::srgb::encode8;
/// Encoded bytes retain the actual existing host lease. No detached byte getter.
pub struct CompletedPng {
+1 -1
View File
@@ -2757,7 +2757,7 @@ mod retained_request_tests {
assert!(frame.linear_exr().is_some());
let color = [128, 64, 255].map(|v| {
let x = crate::surface::srgb8_to_linear(v) as f32 as f64;
solidcraft_geom::srgb::encode8(x / (1.0 + x))
solidcraft_color::srgb::encode8(x / (1.0 + x))
});
assert_eq!(image::load_from_memory(frame.png()).unwrap().to_rgba8().get_pixel(0, 0).0, [color[0], color[1], color[2], 255]);
let held = job.clone();
+1 -1
View File
@@ -375,7 +375,7 @@ impl PhotographicState {
let grain = (random(index as u32 ^ self.options.seed) - 0.5) * self.options.grain * (luminance + 0.03).sqrt();
for channel in 0..3 {
let linear = (rgb[channel] + flare[channel] + grain).max(0.0);
self.output.push(solidcraft_geom::srgb::encode8(linear));
self.output.push(solidcraft_color::srgb::encode8(linear));
}
self.output.push(255);
}
+1 -1
View File
@@ -191,7 +191,7 @@ impl Display {
let strength = self.finishing.bloom.map_or(0.0, |b| b.strength);
value += strength * self.tone_map.map(f64::from(glow[channel]) * self.exposure * self.finishing.rgb_gain[channel]);
}
*out = solidcraft_geom::srgb::encode8(value);
*out = solidcraft_color::srgb::encode8(value);
}
rgba[3] = if self.transparent { (alpha * 255.0).round() as u8 } else { 255 };
}
+1 -1
View File
@@ -32,7 +32,7 @@ pub mod texture_lod;
pub mod texture_sampling;
pub mod transmission;
pub use analysis::{AnalysisColors, AnalysisMode, SceneAnalysis};
pub use analysis::{AnalysisMode, SceneAnalysis};
pub use backend::{
Availability, BackendDescriptor, BackendId, ExecutorDescriptor, ExecutorId, MAX_RENDER_ID_BYTES, RenderFeature, RenderFormat, RenderLimits,
RenderMode, RenderOperation, RenderPolicy, RenderProfile, RenderRequest, ResolveError, ResolvedRequest, ScratchLayout, builtin_backend,
+1 -1
View File
@@ -15,8 +15,8 @@ use crate::{
surface::{OpaqueSurface, srgb8_to_linear},
};
use solidcore_record::Vec3;
use solidcraft_color::srgb::encode8;
use solidcraft_geom::prelude::*;
use solidcraft_geom::srgb::encode8;
#[cfg(test)]
#[path = "environment_image_tests.rs"]
+1 -1
View File
@@ -111,7 +111,7 @@ impl StagePanel {
mesh.tri_face.extend([0, 0]);
// The surface carries exact linear reflectance. This display colour is not
// used as an additional multiplier by PBR consumers.
let color = self.reflectance_linear.map(solidcraft_geom::srgb::encode8);
let color = self.reflectance_linear.map(solidcraft_color::srgb::encode8);
Ok(ProductMesh {
name: format!("__render_stage__/{}", self.id),
mesh: Arc::new(mesh),
+2 -2
View File
@@ -160,8 +160,8 @@ impl OpaqueSurface {
}
}
/// Exact piecewise sRGB decoding of an existing 8-bit color channel ([`solidcraft_geom::srgb::decode8`]).
pub use solidcraft_geom::srgb::decode8 as srgb8_to_linear;
/// Exact piecewise sRGB decoding of an existing 8-bit color channel ([`solidcraft_color::srgb::decode8`]).
pub use solidcraft_color::srgb::decode8 as srgb8_to_linear;
/// Normalize finite directions without overflow or an arbitrary magnitude threshold.
pub fn unit_direction(v: Vec3) -> Result<Vec3, SurfaceError> {
@@ -179,7 +179,7 @@ impl NormalizationSource {
let linear = match conversion {
ColorConversion::RawData => bounded,
ColorConversion::PngGammaSrgbPrimaries { encoding_gamma } => bounded.powf(1.0 / encoding_gamma),
_ => solidcraft_geom::srgb::decode(bounded),
_ => solidcraft_color::srgb::decode(bounded),
};
if !linear.is_finite() || !(0.0..=1.0).contains(&linear) {
return Err(invalid("normalization produced invalid linear/data channels"));
+4 -2
View File
@@ -11,6 +11,10 @@ workspace = true
[dependencies]
solidcore-record = { workspace = true }
solidcraft-engine = { workspace = true }
# The widget registry (handles, rects, counts) the UI kit publishes into.
solidcraft-ui-registry = { workspace = true }
solidcraft-color = { workspace = true }
solidcraft-ui-text = { workspace = true }
egui = { workspace = true }
egui-wgpu = { workspace = true }
solidcraft-gpu = { workspace = true }
@@ -25,10 +29,8 @@ image = { workspace = true, features = ["jpeg", "ico", "gif", "webp", "bmp", "ti
ttf-parser = { workspace = true }
# Right-to-left interface text (Hebrew): the Unicode Bidirectional Algorithm puts translated
# strings in display order, which egui does not do itself (MIT OR Apache-2.0).
unicode-bidi = "0.3"
# The system's preferred languages on a first start (MIT OR Apache-2.0, maintained by
# 1Password): the OS locale on desktop and, with `js`, `navigator.languages` in the browser (#10138).
sys-locale = { version = "0.3.2", features = ["js"] }
semver = { workspace = true }
# Updates: the downloaded package's SHA-256 (against the release's SHA256SUMS.txt), its optional
# minisign signature (MIT), and the portable .zip's executables.
+4 -4
View File
@@ -248,7 +248,7 @@ impl AcpBackend {
// Drain replies first: an acknowledgement already waiting must keep the connection.
if self.cancelling.is_some_and(|(_, started)| now.saturating_duration_since(started) >= CANCEL_GRACE) {
self.proc.shutdown(false);
out.push(self.finish_exit(vec![crate::i18n::tr("ui.agent_chat_cancel_timeout").to_string()]));
out.push(self.finish_exit(vec![solidcraft_ui_text::tr("ui.agent_chat_cancel_timeout").to_string()]));
}
out
}
@@ -359,9 +359,9 @@ pub fn test_agent(argv: &[String], cwd: &std::path::Path) -> Result<String, Test
}
}
Ok(FromAgent::Exited(tail)) => {
return Err(TestError::Other(tail.last().cloned().unwrap_or_else(|| crate::i18n::tr("ui.agent_chat_exited").to_string())));
return Err(TestError::Other(tail.last().cloned().unwrap_or_else(|| solidcraft_ui_text::tr("ui.agent_chat_exited").to_string())));
}
Err(_) => return Err(TestError::Other(crate::i18n::tr("ui.ai_no_answer").to_string())),
Err(_) => return Err(TestError::Other(solidcraft_ui_text::tr("ui.ai_no_answer").to_string())),
}
}
};
@@ -372,7 +372,7 @@ pub fn test_agent(argv: &[String], cwd: &std::path::Path) -> Result<String, Test
p.send(&acp::request(2, "session/new", acp::session_new_params(&session_cwd, None, &v, acp::SYSTEM_NOTE)));
wait_for(&p, 2).map(|_| {
let who = acp::agent_info(&v);
crate::i18n::format("ui.ai_test_acp_ok", &[("agent", if who.is_empty() { "?" } else { &who })])
solidcraft_ui_text::format("ui.ai_test_acp_ok", &[("agent", if who.is_empty() { "?" } else { &who })])
})
});
p.shutdown(true);
+4 -4
View File
@@ -32,7 +32,7 @@ impl Mode {
Mode::ALL.into_iter().find(|m| m.id() == s)
}
pub fn label(self) -> &'static str {
crate::i18n::tr(match self {
solidcraft_ui_text::tr(match self {
Mode::Ask => "ui.ai_mode_ask",
Mode::Edits => "ui.ai_mode_edits",
Mode::All => "ui.ai_mode_all",
@@ -217,7 +217,7 @@ impl Entry {
match self.kind {
Kind::Api | Kind::Anthropic => format!("{} · {}", self.model, self.base_url),
Kind::Acp => match (&self.problem, &self.version) {
(Some(p), _) => crate::i18n::format("ui.agent_chat_cannot_start", &[("error", p)]),
(Some(p), _) => solidcraft_ui_text::format("ui.agent_chat_cannot_start", &[("error", p)]),
(None, Some(v)) => v.clone(),
(None, None) => super::panel::short_command(&self.argv.join(" ")),
},
@@ -228,7 +228,7 @@ impl Entry {
pub fn problem_hint(&self) -> Option<String> {
let problem = self.problem.as_deref()?;
let program = self.argv.first().map(String::as_str).unwrap_or_default();
Some(crate::i18n::format(
Some(solidcraft_ui_text::format(
"ui.agent_chat_reinstall",
&[("agent", &self.name), ("program", program), ("error", problem), ("install", self.install)],
))
@@ -381,7 +381,7 @@ pub fn normalize_url(url: &str) -> Result<String, String> {
let u = url.trim().trim_end_matches('/').to_string();
let lower = u.to_ascii_lowercase();
if !(lower.starts_with("http://") || lower.starts_with("https://")) || u.len() < 10 || u.contains(char::is_whitespace) {
return Err(crate::i18n::tr("ui.ai_bad_url").to_string());
return Err(solidcraft_ui_text::tr("ui.ai_bad_url").to_string());
}
Ok(u)
}
+19 -14
View File
@@ -672,7 +672,7 @@ pub struct ApiBackend {
impl ApiBackend {
pub fn start(cfg: ApiConfig, ctx: StartContext) -> Result<ApiBackend, String> {
if cfg.key.is_some() && !super::agents::key_allowed_for(&cfg.base_url, cfg.protocol == Protocol::OpenAi && cfg.allow_insecure_http) {
return Err(crate::i18n::tr("ui.ai_key_needs_https").to_string());
return Err(solidcraft_ui_text::tr("ui.ai_key_needs_https").to_string());
}
// One stable session ID per chat for OpenCode Go's routing and prompt cache.
let session_id = new_session_id();
@@ -1295,7 +1295,7 @@ impl Worker {
/// What the chat says when a request ran out of time: the limit, and where to raise it.
pub fn timed_out(limit_s: u64) -> String {
crate::i18n::format("ui.ai_request_timed_out", &[("seconds", &limit_s.to_string())])
solidcraft_ui_text::format("ui.ai_request_timed_out", &[("seconds", &limit_s.to_string())])
}
/// A failed request as people read it: one that ran into the endpoint's request timeout (the
@@ -1309,10 +1309,10 @@ pub fn http_error(status: u16, body: &Value) -> String {
let detail =
body.pointer("/error/message").or_else(|| body.get("error")).and_then(Value::as_str).unwrap_or("").chars().take(300).collect::<String>();
let what = match status {
401 | 403 => crate::i18n::tr("ui.ai_http_key"),
404 => crate::i18n::tr("ui.ai_http_not_found"),
429 => crate::i18n::tr("ui.ai_http_busy"),
413 => crate::i18n::tr("ui.ai_http_too_large"),
401 | 403 => solidcraft_ui_text::tr("ui.ai_http_key"),
404 => solidcraft_ui_text::tr("ui.ai_http_not_found"),
429 => solidcraft_ui_text::tr("ui.ai_http_busy"),
413 => solidcraft_ui_text::tr("ui.ai_http_too_large"),
_ => "",
};
format!(
@@ -1643,7 +1643,7 @@ fn anthropic_headers(base_url: &str, key: Option<&str>) -> Result<Vec<(String, S
let mut headers = vec![("anthropic-version".to_string(), ANTHROPIC_VERSION.to_string())];
if let Some(k) = key {
if !super::agents::key_safe_for(base_url) {
return Err(crate::i18n::tr("ui.ai_key_needs_https").to_string());
return Err(solidcraft_ui_text::tr("ui.ai_key_needs_https").to_string());
}
headers.push(("x-api-key".to_string(), k.to_string()));
}
@@ -1670,7 +1670,7 @@ pub fn anthropic_model_list(http: &HttpFn, base_url: &str, key: Option<&str>) ->
pub fn test_anthropic_endpoint(http: &HttpFn, base_url: &str, key: Option<&str>, model: &str) -> Result<String, String> {
let model = model.trim();
if model.is_empty() {
return Err(crate::i18n::tr("ui.ai_need_model").to_string());
return Err(solidcraft_ui_text::tr("ui.ai_need_model").to_string());
}
let mut headers = anthropic_headers(base_url, key)?;
headers.push(("Content-Type".to_string(), "application/json".to_string()));
@@ -1687,7 +1687,7 @@ pub fn test_anthropic_endpoint(http: &HttpFn, base_url: &str, key: Option<&str>,
if status != 200 {
return Err(http_error(status, &v));
}
Ok(crate::i18n::format("ui.ai_test_answered", &[("model", model)]))
Ok(solidcraft_ui_text::format("ui.ai_test_answered", &[("model", model)]))
}
/// A tool call's result as text for the model, and whether it failed.
@@ -1741,7 +1741,7 @@ fn tool_title(name: &str, args: &Value) -> String {
/// endpoint should be, say): an error, so the picker says so and asks again later.
fn listed_models(body: &Value, name_key: &str) -> Result<Vec<(String, Option<String>)>, String> {
let Some(entries) = body.get("data").and_then(Value::as_array) else {
return Err(crate::i18n::tr("ui.agent_chat_models_not_a_list").to_string());
return Err(solidcraft_ui_text::tr("ui.agent_chat_models_not_a_list").to_string());
};
Ok(entries
.iter()
@@ -1771,7 +1771,7 @@ pub fn model_list(http: &HttpFn, base_url: &str, key: Option<&str>, allow_insecu
let mut headers = Vec::new();
if let Some(k) = key {
if !super::agents::key_allowed_for(base_url, allow_insecure_http) {
return Err(crate::i18n::tr("ui.ai_key_needs_https").to_string());
return Err(solidcraft_ui_text::tr("ui.ai_key_needs_https").to_string());
}
headers.push(("Authorization".to_string(), format!("Bearer {k}")));
}
@@ -1783,13 +1783,16 @@ pub fn test_endpoint(http: &HttpFn, base_url: &str, key: Option<&str>, model: &s
let mut headers = Vec::new();
if let Some(k) = key {
if !super::agents::key_allowed_for(base_url, allow_insecure_http) {
return Err(crate::i18n::tr("ui.ai_key_needs_https").to_string());
return Err(solidcraft_ui_text::tr("ui.ai_key_needs_https").to_string());
}
headers.push(("Authorization".to_string(), format!("Bearer {k}")));
}
let models = model_entries(http, base_url, headers)?;
let found = model.is_empty() || models.iter().any(|m| m.id == model);
Ok(crate::i18n::format(if found { "ui.ai_test_ok" } else { "ui.ai_test_no_model" }, &[("count", &models.len().to_string()), ("model", model)]))
Ok(solidcraft_ui_text::format(
if found { "ui.ai_test_ok" } else { "ui.ai_test_no_model" },
&[("count", &models.len().to_string()), ("model", model)],
))
}
#[cfg(test)]
@@ -2642,6 +2645,7 @@ mod tests {
/// id that could not be sent back is left out, and the list stays within its caps.
#[test]
fn a_listing_keeps_the_names_and_stays_within_its_caps() {
crate::i18n::install();
let mut data = vec![
json!({"id": "openai/gpt-x", "name": "OpenAI: GPT X", "description": "Long text", "supported_parameters": ["tools"]}),
json!({"id": "same", "name": "same", "display_name": "Same One"}),
@@ -2675,7 +2679,7 @@ mod tests {
/// picker says why and asks again later; a list with nothing in it is a list.
#[test]
fn an_answer_that_is_no_model_list_is_an_error() {
let expected = crate::i18n::tr("ui.agent_chat_models_not_a_list").to_string();
let expected = solidcraft_ui_text::tr("ui.agent_chat_models_not_a_list").to_string();
let page: HttpFn = Arc::new(|_: &HttpRequest| Ok((200, b"<!doctype html><html><body>Control UI</body></html>".to_vec())));
assert_eq!(model_list(&page, "http://localhost:18789/v1", None, false).unwrap_err(), expected);
assert_eq!(anthropic_model_list(&page, "http://localhost:18789/v1", None).unwrap_err(), expected);
@@ -3110,6 +3114,7 @@ mod tests {
#[test]
fn the_anthropic_test_sends_one_small_message_and_checks_the_key_rule() {
crate::i18n::install();
let seen = Arc::new(std::sync::Mutex::new(Vec::<HttpRequest>::new()));
let log = seen.clone();
let http: HttpFn = Arc::new(move |r: &HttpRequest| {
+7 -3
View File
@@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::backend::{self, PromptPart};
use crate::i18n::format as trf;
use solidcraft_ui_text::format as trf;
/// A refusal in the app's language: the file's name, then why.
fn refused(key: &str, name: &str, args: &[(&str, &str)]) -> String {
@@ -508,6 +508,7 @@ mod tests {
#[test]
fn bad_files_are_refused_with_a_reason() {
crate::i18n::install();
let d = dir("bad");
let fake = d.join("fake.png");
std::fs::write(&fake, b"not a picture at all").unwrap();
@@ -533,15 +534,16 @@ mod tests {
/// Review of #1744: the refusals were English whatever the app's language.
#[test]
fn refusals_speak_the_apps_language() {
crate::i18n::install();
let d = dir("lang");
let binary = d.join("bin.json");
std::fs::write(&binary, [0xff, 0xfe, 0x00, 0x80]).unwrap();
{
let _ja = crate::i18n::scope("ja");
let _ja = solidcraft_ui_text::scope("ja");
assert!(read(&binary).unwrap_err().contains("UTF-8 のテキストではありません"));
}
{
let _zh = crate::i18n::scope("zh");
let _zh = solidcraft_ui_text::scope("zh");
assert!(read(&binary).unwrap_err().contains("不是 UTF-8 文本"));
}
assert!(read(&binary).unwrap_err().contains("not UTF-8 text"), "English again outside the scope");
@@ -552,6 +554,7 @@ mod tests {
/// sent as PNG; SVG goes as its source text; HEIC says why not.
#[test]
fn each_picture_format_goes_the_way_the_agent_reads_it() {
crate::i18n::install();
let d = dir("formats");
let img = image::RgbaImage::from_pixel(30, 20, image::Rgba([20, 120, 220, 255]));
for (file, format, mime) in [
@@ -617,6 +620,7 @@ mod tests {
/// its size and its place) instead of refused; the agent reads it with its own tools.
#[test]
fn other_files_go_by_reference() {
crate::i18n::install();
let d = dir("reference");
let zip = d.join("fixtures.zip");
std::fs::write(&zip, vec![0u8; 3000]).unwrap();
+1 -1
View File
@@ -21,7 +21,7 @@ pub enum TestError {
impl TestError {
pub fn message(&self) -> &str {
match self {
Self::AuthRequired => crate::i18n::tr("ui.agent_chat_state_auth"),
Self::AuthRequired => solidcraft_ui_text::tr("ui.agent_chat_state_auth"),
Self::Other(message) => message,
}
}
+2 -1
View File
@@ -483,7 +483,7 @@ fn detect_in(dirs: &[PathBuf], home: Option<&Path>, limit: Duration) -> Detectio
argv.extend(k.args.iter().map(|a| a.to_string()));
let mut result = if k.reports_version { probe(&path, limit) } else { Probe::default() };
if let Some(version) = result.old_node.as_deref() {
result.problem = Some(crate::i18n::format("ui.agent_chat_node_too_old", &[("version", version)]));
result.problem = Some(solidcraft_ui_text::format("ui.agent_chat_node_too_old", &[("version", version)]));
if uses_env_node(&path)
&& let Some(node) = compatible_node(dirs, limit)
{
@@ -561,6 +561,7 @@ mod tests {
#[test]
#[cfg(unix)]
fn a_node_adapter_recovers_from_an_incompatible_desktop_runtime() {
crate::i18n::install();
use super::super::process::{AgentProcess, FromAgent};
use std::os::unix::fs::PermissionsExt;
@@ -14,8 +14,8 @@ use super::markdown::{self, Block, Span};
use egui::text::{LayoutJob, TextFormat};
use egui::{Color32, FontId, Margin, RichText, Stroke, vec2};
use crate::i18n::tr;
use crate::theme::Tokens;
use solidcraft_ui_text::tr;
/// The parsed blocks of `src`, kept in the context's temporary memory under `slot` (the item's
/// index) and replaced when the text changes.
+3 -3
View File
@@ -45,9 +45,9 @@ use serde_json::{Value, json};
use context::Context;
use crate::i18n::{format as trf, tr};
use crate::{ControlRequest, SolveApp};
use backend::{ChatBackend, Event, PermissionOption, PromptPart, ToolStatus};
use solidcraft_ui_text::{format as trf, tr};
pub(crate) use panel::{finish_shared, left_inset, owns_keyboard_input, prepare_shared, request_new_chat, shared_content, tool_space};
pub use panel::{show, tip as panel_tip, toggle, toggle_shortcut};
@@ -565,11 +565,11 @@ impl AgentChat {
job: impl FnOnce() -> std::result::Result<attach::Attachment, String> + Send + 'static,
) -> std::result::Result<String, String> {
let (tx, rx) = std::sync::mpsc::channel();
let language = crate::i18n::language();
let language = solidcraft_ui_text::language();
std::thread::Builder::new()
.name("chat-file".into())
.spawn(move || {
let _language = crate::i18n::scope(language);
let _language = solidcraft_ui_text::scope(language);
let _ = tx.send(job());
})
.map_err(|e| trf("ui.agent_chat_file_error", &[("name", &name), ("error", &e.to_string())]))?;
+5 -5
View File
@@ -260,7 +260,7 @@ pub(crate) fn left_inset(app: &SolveApp, available: Rect) -> f32 {
&& app.preferences.agent_chat_side == "left"
{
let width = if app.preferences.agent_chat_minimized { STRIP } else { app.preferences.agent_chat_width.clamp(MIN_WIDTH, MAX_WIDTH) };
(width + crate::shell::SHELL_GAP).min(available.width().max(0.0))
(width + crate::ui_kit::SHELL_GAP).min(available.width().max(0.0))
} else {
0.0
}
@@ -276,9 +276,9 @@ pub(crate) fn tool_space(app: &SolveApp, mut available: Rect) -> Rect {
&& let Some(chat) = app.agent_chat.panel_rect.filter(|r| r.is_finite() && r.is_positive())
{
if app.preferences.agent_chat_side == "left" {
available.min.x = crate::shell::within(chat.right() + crate::shell::SHELL_GAP, available.left(), available.right());
available.min.x = crate::ui_kit::within(chat.right() + crate::ui_kit::SHELL_GAP, available.left(), available.right());
} else {
available.max.x = crate::shell::within(chat.left() - crate::shell::SHELL_GAP, available.left(), available.right());
available.max.x = crate::ui_kit::within(chat.left() - crate::ui_kit::SHELL_GAP, available.left(), available.right());
}
}
available
@@ -287,7 +287,7 @@ pub(crate) fn tool_space(app: &SolveApp, mut available: Rect) -> Rect {
/// The same outer rectangle is used for the floating sheet and its docking preview.
fn floating_bounds(app: &SolveApp, available: Rect, left: bool) -> Rect {
let t = Tokens::get();
let gap = crate::shell::SHELL_GAP;
let gap = crate::ui_kit::SHELL_GAP;
let mut bounds = available.shrink(gap);
// The ribbon already supplies the top gap shared by browser and history cards.
bounds.min.y = available.top();
@@ -1772,7 +1772,7 @@ fn copy_message_menu(ui: &egui::Ui, response: &egui::Response, n: usize, text: &
.frame(ui_kit::Menu::frame())
.open_memory(if opened { Some(egui::SetOpenCommand::Bool(true)) } else { None })
.show(|ui| {
ui_kit::Menu::show(ui, crate::context_menu::LIST_W, |ui| {
ui_kit::Menu::show(ui, crate::ui_kit::LIST_W, |ui| {
let item = ui_kit::MenuItem::new("copy_message", tr("ui.agent_chat_copy_message"), "copy");
let row = item.show(ui);
crate::scenario::publish_handle(&format!("agent_chat:copy_menu:{n}"), row.rect.center());
+30 -30
View File
@@ -208,10 +208,10 @@ fn start_listing(app: &mut SolveApp, ctx: &egui::Context, key: ListingKey, secre
};
let (tx, rx) = std::sync::mpsc::channel();
let (protocol, url, allow_insecure_http) = (key.0, key.1.clone(), key.3);
let language = crate::i18n::language();
let language = solidcraft_ui_text::language();
let wake = ctx.clone();
let spawned = std::thread::Builder::new().name("endpoint-models".into()).spawn(move || {
let _language = crate::i18n::scope(language);
let _language = solidcraft_ui_text::scope(language);
let list = secret().and_then(|secret| match protocol {
Protocol::OpenAi => super::api_client::model_list(&http, &url, secret.as_deref(), allow_insecure_http),
Protocol::Anthropic => super::api_client::anthropic_model_list(&http, &url, secret.as_deref()),
@@ -519,7 +519,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
permissions_section(app, ui);
#[cfg(not(target_arch = "wasm32"))]
{
let browsing = !crate::prefs_search::querying(ui) || app.prefs_window.section == crate::prefs::PAGE_ASSISTANT;
let browsing = !crate::ui_kit::settings_search::querying(ui) || app.prefs_window.section == crate::prefs::PAGE_ASSISTANT;
if app.preferences.ai_features && browsing && app.agent_chat.detection.is_none() && app.agent_chat.detecting.is_none() {
refresh(app, &ui.ctx().clone());
}
@@ -561,7 +561,7 @@ fn instructions_section(app: &mut SolveApp, ui: &mut egui::Ui) {
let response = ui_kit::text_area(ui, tr("ai.prefs.instructions"), id, text, 6, acp::INSTRUCTIONS_LIMIT);
if response.has_focus() {
// Typed text belongs to the field, not to Preferences search.
crate::prefs_search::note_key_sink(ui.ctx());
crate::ui_kit::settings_search::note_key_sink(ui.ctx());
}
crate::scenario::publish_target("prefs:agent_instructions", response.rect);
ui_kit::text(ui, tr("ai.prefs.instructions_note"), ui_kit::Text::Hint("instructions"));
@@ -658,20 +658,20 @@ fn panel_section(app: &mut SolveApp, ui: &mut egui::Ui) {
let previous = usize::from(p.agent_chat_turn_limit != 0);
let mut limit = previous;
let options = [tr("ui.agent_chat_turn_unlimited"), tr("prefs.assistant_limited")];
crate::prefs_search::note_current(ui, &options.join(" "));
crate::ui_kit::settings_search::note_current(ui, &options.join(" "));
let dropdown_width = ui_kit::ContentWidth::dropdown(ui, &options);
let response =
ui_kit::settings_control_width(ui, dropdown_width, |ui| ui_kit::dropdown(ui, "assistant_turn_limit", &mut limit, &options));
let query = crate::prefs_search::query(ui);
let query = crate::ui_kit::settings_search::query(ui);
if let Some(option) = options
.iter()
.find(|option| option.to_lowercase() == query)
.or_else(|| options.iter().find(|option| crate::prefs_search::texts_hit(ui, &[*option])))
.or_else(|| options.iter().find(|option| crate::ui_kit::settings_search::texts_hit(ui, &[*option])))
{
crate::prefs_search::remember(ui, option, response.rect);
crate::ui_kit::settings_search::remember(ui, option, response.rect);
}
for option in options {
crate::prefs_search::follow(ui, option, response.rect);
crate::ui_kit::settings_search::follow(ui, option, response.rect);
}
crate::scenario::publish_target("prefs:agent_chat_turn_limit:mode", response.rect);
if limit != previous {
@@ -699,14 +699,14 @@ fn pointer_section(app: &mut SolveApp, ui: &mut egui::Ui) {
ui,
|ui| {
let response = ui_kit::checkbox(ui, tr("ai.prefs.show_pointer"), &mut app.ui.agent_cursor.show)
.backing_on_hover_text(crate::i18n::tr_label("Show the pointer an AI agent moves (#35)"));
.backing_on_hover_text(solidcraft_ui_text::tr_label("Show the pointer an AI agent moves (#35)"));
crate::scenario::publish_target("prefs:agent_pointer_show", response.rect);
ui.add_enabled_ui(app.ui.agent_cursor.show, |ui| {
let speed_label_width = ui_kit::ContentWidth::label(ui, tr("ai.prefs.pointer_speed")) + crate::theme::Tokens::get().space_4;
ui_kit::settings_form(ui, speed_label_width, |ui| {
ui_kit::row(ui, tr("ai.prefs.pointer_speed"), |ui| {
let name = tr("prefs.assistant_pointer_speed");
crate::prefs_search::note_current(ui, name);
crate::ui_kit::settings_search::note_current(ui, name);
let response =
ui_kit::named_control(ui, name, |ui| crate::agent_cursor::speed_slider(ui, &mut app.ui.agent_cursor.speed));
crate::scenario::publish_target("prefs:agent_pointer_speed", response.rect);
@@ -717,11 +717,11 @@ fn pointer_section(app: &mut SolveApp, ui: &mut egui::Ui) {
response.widget_info(|| egui::WidgetInfo::slider(ui.is_enabled(), value, name));
// The visible row owns Speed and its legacy Motion speed alias. The
// native editor's name owns queries that only match Pointer speed.
if !crate::prefs_search::texts_hit(ui, &[tr("ai.prefs.pointer_speed")]) {
crate::prefs_search::remember(ui, name, response.rect);
if !crate::ui_kit::settings_search::texts_hit(ui, &[tr("ai.prefs.pointer_speed")]) {
crate::ui_kit::settings_search::remember(ui, name, response.rect);
}
for label in [tr("ai.prefs.pointer_speed"), name] {
crate::prefs_search::follow(ui, label, response.rect);
crate::ui_kit::settings_search::follow(ui, label, response.rect);
}
});
});
@@ -734,7 +734,7 @@ fn pointer_section(app: &mut SolveApp, ui: &mut egui::Ui) {
});
(app.ui.agent_cursor.show, app.ui.agent_cursor.speed.label(), app.ui.agent_cursor.follow_camera)
},
|ui, (visible, speed, follow)| ui_kit::settings_pointer_preview(ui, visible, crate::i18n::tr_label(&speed), follow),
|ui, (visible, speed, follow)| ui_kit::settings_pointer_preview(ui, visible, solidcraft_ui_text::tr_label(&speed), follow),
);
});
}
@@ -781,12 +781,12 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
for entry in &all {
ui.push_id(&entry.id, |ui| {
let detail = entry.detail();
let gate = crate::prefs_search::block_gate(ui, &entry.name, Some(&format!("{} {detail}", entry.id)));
let before = crate::prefs_search::hits(ui);
let gate = crate::ui_kit::settings_search::block_gate(ui, &entry.name, Some(&format!("{} {detail}", entry.id)));
let before = crate::ui_kit::settings_search::hits(ui);
let mut paint = |ui: &mut egui::Ui| {
let card = ui_kit::settings_card(ui, |ui| {
ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
crate::prefs_search::visible(ui, &[&entry.name, &entry.id, &detail]);
crate::ui_kit::settings_search::visible(ui, &[&entry.name, &entry.id, &detail]);
ui.label(egui::RichText::new(&entry.name).strong().size(t.font_label).color(t.form_ink));
ui.allocate_ui_with_layout(
egui::vec2(ui.available_width(), t.control_height),
@@ -851,7 +851,7 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
let shown = current.map_or_else(|| tr("ui.ai_mode_global").to_owned(), |mode| mode.label().to_owned());
let mut pick: Option<Option<Mode>> = None;
let options: Vec<_> = std::iter::once(tr("ui.ai_mode_global")).chain(Mode::ALL.map(Mode::label)).collect();
crate::prefs_search::note_current(ui, &options.join(" "));
crate::ui_kit::settings_search::note_current(ui, &options.join(" "));
let response = ui_kit::settings_control_width(ui, t.space_24 * 9.0 + t.space_4, |ui| {
ui_kit::dropdown_menu(ui, ("sc_ai_mode", &entry.id), &shown, |ui| {
if ui_kit::list_picker(ui, "global", tr("ui.ai_mode_global"), current.is_none()).clicked() {
@@ -864,16 +864,16 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
}
})
});
let query = crate::prefs_search::query(ui);
let query = crate::ui_kit::settings_search::query(ui);
if let Some(option) = options
.iter()
.find(|option| option.to_lowercase() == query)
.or_else(|| options.iter().find(|option| crate::prefs_search::texts_hit(ui, &[*option])))
.or_else(|| options.iter().find(|option| crate::ui_kit::settings_search::texts_hit(ui, &[*option])))
{
crate::prefs_search::remember(ui, option, response.rect);
crate::ui_kit::settings_search::remember(ui, option, response.rect);
}
for option in options {
crate::prefs_search::follow(ui, option, response.rect);
crate::ui_kit::settings_search::follow(ui, option, response.rect);
}
match pick {
Some(Some(mode)) => {
@@ -965,23 +965,23 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
});
crate::scenario::publish_rect(&format!("prefs:ai_card:{}", entry.id), card.response.rect);
let metadata = [&entry.name, &entry.id, &detail];
if let Some(text) = metadata.iter().find(|text| crate::prefs_search::texts_hit(ui, &[*text])) {
crate::prefs_search::remember(ui, text, card.response.rect);
if let Some(text) = metadata.iter().find(|text| crate::ui_kit::settings_search::texts_hit(ui, &[*text])) {
crate::ui_kit::settings_search::remember(ui, text, card.response.rect);
}
for text in metadata {
crate::prefs_search::follow(ui, text, card.response.rect);
crate::ui_kit::settings_search::follow(ui, text, card.response.rect);
}
};
let show = gate.as_ref().is_none_or(|gate| gate.show);
if show {
paint(ui);
} else {
crate::prefs_search::run_hidden(ui, &entry.id, |ui| {
crate::ui_kit::settings_search::run_hidden(ui, &entry.id, |ui| {
paint(ui);
});
}
if let Some(gate) = gate {
crate::prefs_search::block_finish(ui, &gate, show, before);
crate::ui_kit::settings_search::block_finish(ui, &gate, show, before);
}
});
}
@@ -1524,7 +1524,7 @@ mod card_tests {
#[test]
fn pointer_speed_search_reveals_one_real_slider_in_all_enablement_states() {
use crate::prefs_search::{nav, set_cursor};
use crate::ui_kit::settings_search::{nav, set_cursor};
for theme in ["light", "dark"] {
for (ai, show) in [(true, true), (true, false), (false, true), (false, false)] {
let mut h = Harness::new();
@@ -1569,7 +1569,7 @@ mod card_tests {
#[test]
fn overlapping_agent_metadata_has_one_card_target_and_keeps_fields_indexed() {
use crate::prefs_search::{nav, set_cursor};
use crate::ui_kit::settings_search::{nav, set_cursor};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
+1 -1
View File
@@ -8,7 +8,7 @@
use serde_json::{Map, Value};
use crate::i18n::{format as trf, tr, tr_label};
use solidcraft_ui_text::{format as trf, tr, tr_label};
/// Steps of a batch named before "and N more".
const MAX_STEPS: usize = 4;
+10 -10
View File
@@ -1,7 +1,7 @@
use super::*;
use std::sync::mpsc::channel;
fn transcript_clipboard_harness(layout: crate::shell::LayoutMode) -> crate::scenario::Harness {
fn transcript_clipboard_harness(layout: crate::ui_kit::LayoutMode) -> crate::scenario::Harness {
let mut h = narrow_chat();
h.app.ui.layout = layout;
h.app.preferences.agent_chat_width = 500.0;
@@ -36,7 +36,7 @@ fn transcript_click(h: &mut crate::scenario::Harness, pos: egui::Pos2) {
#[test]
fn transcript_copy_and_cut_do_not_touch_the_cad_clipboard() {
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
let mut h = transcript_clipboard_harness(layout);
let at = transcript_text_position(&h);
transcript_click(&mut h, at);
@@ -57,7 +57,7 @@ fn transcript_copy_and_cut_do_not_touch_the_cad_clipboard() {
#[test]
fn transcript_paste_does_not_insert_a_previously_copied_body() {
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
let mut h = transcript_clipboard_harness(layout);
h.frame_with(vec![egui::Event::Copy]);
assert!(!h.app.copied_bodies.is_empty(), "prime the real CAD clipboard");
@@ -77,7 +77,7 @@ fn transcript_paste_does_not_insert_a_previously_copied_body() {
#[test]
fn transcript_key_events_do_not_run_cad_shortcuts() {
for layout in [crate::shell::LayoutMode::Docked, crate::shell::LayoutMode::Floating] {
for layout in [crate::ui_kit::LayoutMode::Docked, crate::ui_kit::LayoutMode::Floating] {
let mut h = transcript_clipboard_harness(layout);
let at = transcript_text_position(&h);
transcript_click(&mut h, at);
@@ -106,7 +106,7 @@ fn transcript_key_events_do_not_run_cad_shortcuts() {
#[test]
fn transcript_shortcut_ownership_ends_when_the_panel_is_hidden() {
for hidden in ["closed", "minimized", "disabled"] {
let mut h = transcript_clipboard_harness(crate::shell::LayoutMode::Floating);
let mut h = transcript_clipboard_harness(crate::ui_kit::LayoutMode::Floating);
h.frame_with(vec![egui::Event::Copy]);
let at = transcript_text_position(&h);
transcript_click(&mut h, at);
@@ -2906,7 +2906,7 @@ fn pictures_are_read_off_the_window_thread() {
send(&mut app, &ctx, words);
assert_eq!(script.prompts().len(), 1);
{
let _ja = crate::i18n::scope("ja");
let _ja = solidcraft_ui_text::scope("ja");
control(&mut app, &ctx, &json!({"attach": at(&d, "fake.png")})).unwrap();
}
settle_files(&mut app, &ctx);
@@ -3184,7 +3184,7 @@ fn a_press_at_the_edge_without_a_drag_towards_it_scrolls_nothing() {
/// at the panel's edge or is cut off at the other one.
#[test]
fn the_agent_list_keeps_the_panels_side_margin() {
use crate::shell::LayoutMode;
use crate::ui_kit::LayoutMode;
for layout in [LayoutMode::Docked, LayoutMode::Floating] {
let mut h = narrow_chat();
h.app.ui.layout = layout;
@@ -4614,9 +4614,9 @@ fn the_composer_hint_stays_on_one_row_at_the_narrowest_width() {
continue;
}
let language = path.file_stem().unwrap().to_string_lossy().into_owned();
let _scope = crate::i18n::scope(&language);
let _scope = solidcraft_ui_text::scope(&language);
for key in ["ui.agent_chat_placeholder", "ui.agent_chat_placeholder_busy"] {
let hint = crate::i18n::tr(key).to_owned();
let hint = solidcraft_ui_text::tr(key).to_owned();
let mut rows = 0;
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
rows = crate::ui_kit::assistant_hint_galley(ui, &hint, width - 2.0 * crate::theme::Tokens::get().form_inset).rows.len();
@@ -4634,7 +4634,7 @@ fn the_composer_hint_stays_on_one_row_at_the_narrowest_width() {
#[test]
fn tool_room_beside_the_docked_assistant_survives_an_inside_out_view() {
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.ui.layout = crate::shell::LayoutMode::Floating;
app.ui.layout = crate::ui_kit::LayoutMode::Floating;
app.preferences.ai_features = true;
app.agent_chat.open = true;
app.preferences.agent_chat_popped = false;
+2 -2
View File
@@ -109,7 +109,7 @@ pub fn speed_slider(ui: &mut egui::Ui, speed: &mut Speed) -> egui::Response {
1.0..=5.5,
crate::ui_kit::Slider {
step: Some(0.5),
format: |v| if v > 5.25 { crate::i18n::tr_label("Instant").into() } else { format!("{v:.2}x") },
format: |v| if v > 5.25 { solidcraft_ui_text::tr_label("Instant").into() } else { format!("{v:.2}x") },
..Default::default()
},
);
@@ -504,7 +504,7 @@ pub fn paint(app: &SolveApp, ctx: &egui::Context) {
.show(ctx, |ui| {
let frame = Container::popup_frame(ui.style()).fill(t.overlay);
Container::show_backing(ui, frame, |ui| {
ui.label(egui::RichText::new(crate::i18n::tr_label(label)).size(t.font_label).color(t.text));
ui.label(egui::RichText::new(solidcraft_ui_text::tr_label(label)).size(t.font_label).color(t.text));
});
});
}
+1 -1
View File
@@ -118,7 +118,7 @@ pub fn tick(app: &mut SolveApp) {
}
let _ = std::fs::write(&file, serde_json::to_string(&all).unwrap_or_default());
let names = fresh.iter().map(|c| client_name(c)).collect::<Vec<_>>().join(", ");
app.set_status(crate::i18n::format("ai.connected_toast", &[("clients", &names)]), false);
app.set_status(solidcraft_ui_text::format("ai.connected_toast", &[("clients", &names)]), false);
}
Err(std::sync::mpsc::TryRecvError::Empty) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => app.ai_connect.job = None,
+5 -4
View File
@@ -115,7 +115,7 @@ fn cell<R>(ui: &mut egui::Ui, width: f32, add: impl FnOnce(&mut egui::Ui) -> R)
/// The label of one of [`BOM_KEYS`].
fn field_label(key: &str) -> &str {
metadata::key(key).map_or(key, |k| crate::i18n::tr(k.label))
metadata::key(key).map_or(key, |k| solidcraft_ui_text::tr(k.label))
}
/// Apply the picked row's fields to each of its definitions (one undo step each).
@@ -146,8 +146,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let bom = d.rows(app);
let rows: Vec<Value> = bom["rows"].as_array().cloned().unwrap_or_default();
let (mut export, mut apply_row) = (None, None);
let closed =
crate::ui_kit::Window::new(egui::Id::unique("sc_bom"), "bom", crate::i18n::tr_label("Bill of Materials")).cards(3.0).show_body(ctx, |ui| {
let closed = crate::ui_kit::Window::new(egui::Id::unique("sc_bom"), "bom", solidcraft_ui_text::tr_label("Bill of Materials"))
.cards(3.0)
.show_body(ctx, |ui| {
ui.horizontal_wrapped(|ui| {
let was = (d.top, d.by_definition, d.group_children);
let mut levels = usize::from(d.top);
@@ -166,7 +167,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
});
d.by_definition = grouping == 1;
ui.separator();
crate::ui_kit::checkbox(ui, crate::i18n::tr("bom.group_children"), &mut d.group_children);
crate::ui_kit::checkbox(ui, solidcraft_ui_text::tr("bom.group_children"), &mut d.group_children);
if was != (d.top, d.by_definition, d.group_children) {
d.picked = None;
}
+19 -13
View File
@@ -148,8 +148,9 @@ use serde_json::{Value, json};
use solidcraft_engine::Sel;
use solidcraft_engine::doc::{BrowserEntry, Document, FeatureKind};
use crate::context_menu::{Item, Target};
use crate::context_menu::Target;
use crate::theme::Tokens;
use crate::ui_kit::Item;
use crate::SolveApp;
@@ -1392,11 +1393,11 @@ fn browser_collapsed_rail(app: &mut SolveApp, ui: &mut egui::Ui) {
crate::ui_kit::disclosure(ui.painter(), disclosure.center(), false, t.text_dim);
crate::scenario::publish_handle("left-browser-toggle", disclosure.center());
crate::scenario::publish_target("browser:header", disclosure);
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), crate::i18n::tr("ui.browser_title")));
if response.backing_on_hover_text(crate::i18n::tr("ui.browser_expand")).clicked() {
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), solidcraft_ui_text::tr("ui.browser_title")));
if response.backing_on_hover_text(solidcraft_ui_text::tr("ui.browser_expand")).clicked() {
app.preferences.timeline_left_browser_collapsed = false;
}
let galley = ui.painter().layout_no_wrap(crate::i18n::tr("ui.browser_title").to_owned(), crate::fonts::semibold(t.font_body), t.text);
let galley = ui.painter().layout_no_wrap(solidcraft_ui_text::tr("ui.browser_title").to_owned(), crate::fonts::semibold(t.font_body), t.text);
let title = Rect::from_min_max(rect.min + vec2(0.0, title_top), rect.max);
ui.painter().add(
egui::epaint::TextShape::new(title.center() - galley.size() / 2.0, galley, t.text)
@@ -1483,7 +1484,7 @@ fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) {
if collapsible && (response.hovered() || response.has_focus()) {
crate::ui_kit::panel_header_hover(ui.painter(), rect);
}
let title = crate::i18n::tr("ui.browser_title").to_string();
let title = solidcraft_ui_text::tr("ui.browser_title").to_string();
let kind = if collapsible { egui::Role::Button } else { egui::Role::Label };
response.widget_info(|| egui::WidgetInfo::labeled(kind, true, &title));
let galley = ui.painter().layout_no_wrap(title, crate::fonts::semibold(t.font_heading), t.text);
@@ -1495,11 +1496,11 @@ fn browser_header(app: &mut SolveApp, ui: &mut egui::Ui, collapsible: bool) {
}
let bodies = app.session.model.state().bodies.len();
let key = if bodies == 1 { "ui.browser_bodies_one" } else { "ui.browser_bodies_other" };
let count = crate::i18n::format(key, &[("count", &bodies.to_string())]);
let count = solidcraft_ui_text::format(key, &[("count", &bodies.to_string())]);
let galley = ui.painter().layout_no_wrap(count, FontId::proportional(t.font_body), t.text_dim);
ui.painter().galley(pos2(right - galley.size().x, rect.center().y - galley.size().y / 2.0), galley, t.text_dim);
if collapsible {
let tip = crate::i18n::tr(if collapsed { "ui.browser_expand" } else { "ui.browser_collapse" });
let tip = solidcraft_ui_text::tr(if collapsed { "ui.browser_expand" } else { "ui.browser_collapse" });
if response.on_hover_cursor(egui::CursorIcon::PointingHand).backing_on_hover_text(tip).clicked() {
if stacked {
app.preferences.timeline_left_browser_collapsed = !app.preferences.timeline_left_browser_collapsed;
@@ -2467,7 +2468,7 @@ fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut A
}
}
if r.double && locked {
app.set_status(crate::i18n::tr("browser.canvas_locked"), true);
app.set_status(solidcraft_ui_text::tr("browser.canvas_locked"), true);
} else if r.double {
app.tree.canvas_panel = Some(CanvasPanel::new(app, id, false));
}
@@ -2491,7 +2492,7 @@ pub fn canvas_items(app: &SolveApp, id: u64) -> Vec<Item> {
let visible = canvas.is_some_and(|c| c.visible);
// A locked canvas (#12310) is not moved, calibrated or deleted until it is unlocked.
let locked = canvas.is_some_and(|c| c.locked);
let why = locked.then(|| crate::i18n::tr("browser.canvas_locked").to_string());
let why = locked.then(|| solidcraft_ui_text::tr("browser.canvas_locked").to_string());
vec![
Item::action("ui.editCanvas", "Edit Canvas", "canvas").with(json!({ "canvas": id })).off_because(why.clone()),
Item::action("ui.calibrateCanvas", "Calibrate", "measure").with(json!({ "canvas": id })).off_because(why.clone()),
@@ -2631,7 +2632,11 @@ impl Entry {
row_key: format!("b:{world}"),
patterns: None,
// A part the modelling kernel refuses says so (Repair Part is in its menu).
label: if b.body.smlib_refused().is_some() { crate::i18n::format("browser.needs_repair", &[("name", &b.name)]) } else { b.name.clone() },
label: if b.body.smlib_refused().is_some() {
solidcraft_ui_text::format("browser.needs_repair", &[("name", &b.name)])
} else {
b.name.clone()
},
icon,
tip: Some(tip),
visible: app.body_visible(&world),
@@ -2657,9 +2662,9 @@ impl Entry {
thing: Thing::Sketch(f.id),
patterns: crate::sketch_pattern_tree::has_patterns(app, f.id).then(|| format!("s:{}/patterns", f.id)),
label: if lost {
crate::i18n::format("browser.sketch_attachment_lost", &[("name", &crate::i18n::tr_name(&f.name))])
solidcraft_ui_text::format("browser.sketch_attachment_lost", &[("name", &crate::i18n::tr_name(&f.name))])
} else if active {
crate::i18n::format("browser.sketch_editing", &[("name", &crate::i18n::tr_name(&f.name))])
solidcraft_ui_text::format("browser.sketch_editing", &[("name", &crate::i18n::tr_name(&f.name))])
} else {
f.name.clone()
},
@@ -3023,7 +3028,8 @@ fn folder_level(
} else {
&group.label
};
let count = if render && !gopen { crate::i18n::format("render.tree_more", &[("count", &group.count.to_string())]) } else { String::new() };
let count =
if render && !gopen { solidcraft_ui_text::format("render.tree_more", &[("count", &group.count.to_string())]) } else { String::new() };
let r = draw_row(
ui,
ui.scope_id().with(gkey),
+2 -2
View File
@@ -95,7 +95,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
let scope = if path.is_empty() { format!("c{component}") } else { occurrence_key(path) };
// The history opens by default; the Features folder stays closed until opened.
let (key, label) =
if compact { (format!("{scope}/history"), crate::i18n::tr("ui.history_title")) } else { (format!("{scope}/features"), "Features") };
if compact { (format!("{scope}/history"), solidcraft_ui_text::tr("ui.history_title")) } else { (format!("{scope}/features"), "Features") };
let open = is_open(app, &key, compact);
// Folded, it carries the worst mark of what it holds. A row scrolled out of view paints
// nothing, so its mark is not worked out: in a long tree of placements of a part with a long
@@ -400,7 +400,7 @@ fn marker_row(app: &SolveApp, ui: &mut egui::Ui, depth: usize) -> egui::Response
if response.hovered() || response.dragged() {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
}
response.backing_on_hover_text(crate::i18n::tr("ui.history_marker_tip"))
response.backing_on_hover_text(solidcraft_ui_text::tr("ui.history_marker_tip"))
}
/// Dragging the marker shows where it lands; letting go rolls the history there, through the
+19 -18
View File
@@ -12,8 +12,9 @@ use solidcraft_engine::view::POINT_CLOUDS_FOLDER;
use super::{Actions, Row, draw_row, is_open, panel_pos, rows_start_open, toggle, toggle_folder};
use crate::SolveApp;
use crate::context_menu::{Item, Target};
use crate::context_menu::Target;
use crate::ui_kit;
use crate::ui_kit::Item;
#[path = "point_cloud_tracks.rs"]
pub(crate) mod point_cloud_tracks;
@@ -267,7 +268,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
};
let mut edit: Option<Value> = None;
let mut reload = false;
let title = crate::i18n::format("browser.edit_point_cloud_title", &[("name", &c.name)]);
let title = solidcraft_ui_text::format("browser.edit_point_cloud_title", &[("name", &c.name)]);
let auto = if c.color_by == ColorBy::Height { m.auto_range(&c) } else { None };
let dots = m.dots(&c);
let now = ctx.input(|i| i.time);
@@ -299,16 +300,16 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_point_cloud_panel"), "point", &title)
.placed(egui::Align2::LEFT_TOP, at)
.fields()
.actions("", crate::i18n::tr("ui.close"), None)
.actions("", solidcraft_ui_text::tr("ui.close"), None)
.show(ctx, true, |ui| {
ui_kit::row(ui, crate::i18n::tr_label("Points"), |ui| ui.label(c.len().to_string()));
ui_kit::row(ui, solidcraft_ui_text::tr_label("Points"), |ui| ui.label(c.len().to_string()));
// The dots' size, where some points are dots: those no segment or triangle ends on.
if dots > 0 {
if dots < c.len() {
ui_kit::row(ui, crate::i18n::tr_label("Dots"), |ui| ui.label(dots.to_string()));
ui_kit::row(ui, solidcraft_ui_text::tr_label("Dots"), |ui| ui.label(dots.to_string()));
}
let mut size = c.point_size;
if ui_kit::row(ui, crate::i18n::tr("ui.point_cloud_size"), |ui| ui_kit::slider(ui, &mut size, 0.5..=16.0, Default::default()))
if ui_kit::row(ui, solidcraft_ui_text::tr("ui.point_cloud_size"), |ui| ui_kit::slider(ui, &mut size, 0.5..=16.0, Default::default()))
.changed()
{
edit = Some(json!({ "cloud": c.id, "point_size": size }));
@@ -318,7 +319,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
let was = units.iter().position(|u| *u == c.units).unwrap_or(0);
let mut unit = was;
// A dropdown's response doesn't report the pick: compare the index.
ui_kit::row(ui, crate::i18n::tr("ui.point_cloud_units"), |ui| ui_kit::dropdown(ui, "point_cloud_units", &mut unit, &units));
ui_kit::row(ui, solidcraft_ui_text::tr("ui.point_cloud_units"), |ui| ui_kit::dropdown(ui, "point_cloud_units", &mut unit, &units));
if unit != was
&& let Some(u) = units.get(unit)
{
@@ -334,7 +335,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
by => COLOR_BY.iter().position(|(b, _)| *b == by).unwrap_or(0),
};
let mut by = was;
ui_kit::row(ui, crate::i18n::tr("ui.point_cloud_color_by"), |ui| ui_kit::dropdown(ui, "point_cloud_color_by", &mut by, &labels));
ui_kit::row(ui, solidcraft_ui_text::tr("ui.point_cloud_color_by"), |ui| ui_kit::dropdown(ui, "point_cloud_color_by", &mut by, &labels));
if by != was {
let name = COLOR_BY.get(by).map(|(b, _)| b.name()).or_else(|| properties.get(by - COLOR_BY.len()).copied());
if let Some(name) = name {
@@ -345,12 +346,12 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
let legend_shown = colored.and_then(|n| label(&c, n));
match c.color_by {
ColorBy::File if c.colors.is_empty() => {
ui_kit::row(ui, "", |ui| ui.weak(crate::i18n::tr("ui.point_cloud_no_file_colors")));
ui_kit::row(ui, "", |ui| ui.weak(solidcraft_ui_text::tr("ui.point_cloud_no_file_colors")));
}
ColorBy::File => {}
ColorBy::Single => {
let mut rgb = c.color.unwrap_or(PLAIN).map(byte);
if ui_kit::row(ui, crate::i18n::tr_label("Colour"), |ui| ui_kit::color_field(ui, &mut rgb)).changed() {
if ui_kit::row(ui, solidcraft_ui_text::tr_label("Colour"), |ui| ui_kit::color_field(ui, &mut rgb)).changed() {
edit = Some(json!({ "cloud": c.id, "color": rgb.map(|v| f64::from(v) / 255.0) }));
}
}
@@ -366,7 +367,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
{
// A ramp over what is shown, unless its range is set.
let automatic = c.height_range.is_none();
if ui_kit::checkbox_row(ui, "point_cloud_auto_range", crate::i18n::tr("ui.point_cloud_auto_range"), automatic) {
if ui_kit::checkbox_row(ui, "point_cloud_auto_range", solidcraft_ui_text::tr("ui.point_cloud_auto_range"), automatic) {
edit = Some(json!({ "cloud": c.id, "color_range": if automatic { json!(crate::point_clouds::color_range(ui.ctx(), &live)) } else { Value::Null } }));
}
// The ramp's range is a range on a track of the property, over what the
@@ -422,9 +423,9 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
&& ui_kit::button(
ui,
if playing {
format!("⏸ {}", crate::i18n::tr_label("Pause"))
format!("⏸ {}", solidcraft_ui_text::tr_label("Pause"))
} else {
format!("▶ {}", crate::i18n::tr_label("Play"))
format!("▶ {}", solidcraft_ui_text::tr_label("Play"))
},
ui_kit::Button::Small,
)
@@ -442,7 +443,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
match (&f.keep, span) {
(Keep::Values { .. }, _) => match label(&c, &f.property) {
Some(names) if legend_shown.is_some_and(|l| l.property == names.property) => {
ui.weak(crate::i18n::tr_label("Shown and hidden in the legend above"));
ui.weak(solidcraft_ui_text::tr_label("Shown and hidden in the legend above"));
}
Some(names) => {
if let Some(v) = legend(ui, &c, names) {
@@ -520,10 +521,10 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
}
let all: Vec<&str> = c.property_names().collect();
if c.filters.len() < MAX_FILTERS {
let mut add_labels = vec![crate::i18n::tr_label("Add filter…")];
let mut add_labels = vec![solidcraft_ui_text::tr_label("Add filter…")];
add_labels.extend(all.iter().copied());
let mut pick = 0;
ui_kit::row(ui, crate::i18n::tr_label("Filter by"), |ui| ui_kit::dropdown(ui, "point_cloud_add_filter", &mut pick, &add_labels));
ui_kit::row(ui, solidcraft_ui_text::tr_label("Filter by"), |ui| ui_kit::dropdown(ui, "point_cloud_add_filter", &mut pick, &add_labels));
if let Some(name) = all.get(pick.wrapping_sub(1)) {
// A label keeps all its values at first, anything else its whole span.
let keep = match (label(&c, name), c.span(name)) {
@@ -540,7 +541,7 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
if changed {
edit = Some(json!({ "cloud": c.id, "filters": filters_json(&filters) }));
}
if !c.source.is_empty() && ui_kit::button(ui, crate::i18n::tr("ui.point_cloud_reload"), ui_kit::Button::Normal).clicked() {
if !c.source.is_empty() && ui_kit::button(ui, solidcraft_ui_text::tr("ui.point_cloud_reload"), ui_kit::Button::Normal).clicked() {
reload = true;
}
});
@@ -599,7 +600,7 @@ mod tests {
}
h.app.run("pointcloud.edit", json!({"cloud": id, "color_by": "height"})).unwrap();
h.frames(2);
assert!(h.widget(crate::i18n::tr("ui.point_cloud_auto_range")).is_some());
assert!(h.widget(solidcraft_ui_text::tr("ui.point_cloud_auto_range")).is_some());
h.step(&json!({"widget": "Close"})).unwrap();
h.frames(2);
assert!(h.app.tree.cloud_panel.is_none());
+19 -13
View File
@@ -22,10 +22,14 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
};
let mut edit: Option<Value> = None;
let (units, decimals) = (app.session.doc.units.clone(), app.preferences.length_decimals);
let title = crate::i18n::format(if m.calibrate { "browser.calibrate_title" } else { "browser.edit_canvas_title" }, &[("name", &c.name)]);
let title = solidcraft_ui_text::format(if m.calibrate { "browser.calibrate_title" } else { "browser.edit_canvas_title" }, &[("name", &c.name)]);
let at = panel_pos(app);
let (calibrate, name) = (m.calibrate, c.id);
let (ok, cancel) = if calibrate { (crate::i18n::tr_label("Calibrate"), crate::i18n::tr("ui.cancel")) } else { ("", crate::i18n::tr("ui.close")) };
let (ok, cancel) = if calibrate {
(solidcraft_ui_text::tr_label("Calibrate"), solidcraft_ui_text::tr("ui.cancel"))
} else {
("", solidcraft_ui_text::tr("ui.close"))
};
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_canvas_panel"), "canvas", &title)
.placed(egui::Align2::LEFT_TOP, at)
.fields()
@@ -34,11 +38,11 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
if calibrate {
ui_kit::text(
ui,
crate::i18n::tr(if m.next == 0 { "browser.calibrate_click_a" } else { "browser.calibrate_click_b" }),
solidcraft_ui_text::tr(if m.next == 0 { "browser.calibrate_click_a" } else { "browser.calibrate_click_b" }),
ui_kit::Text::Note("calibrate_hint"),
);
for (label, p) in [("Point A", &mut m.a), ("Point B", &mut m.b)] {
ui_kit::row(ui, crate::i18n::tr_label(label), |ui| {
ui_kit::row(ui, solidcraft_ui_text::tr_label(label), |ui| {
let [x, y] = p;
ui.horizontal(|ui| {
ui_kit::length_field(ui, x, &units, decimals);
@@ -46,18 +50,20 @@ pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
});
});
}
ui_kit::row(ui, crate::i18n::tr_label("Distance"), |ui| ui_kit::length_field(ui, &mut m.distance, &units, decimals));
ui_kit::row(ui, solidcraft_ui_text::tr_label("Distance"), |ui| ui_kit::length_field(ui, &mut m.distance, &units, decimals));
m.distance = m.distance.clamp(0.001, 1e6);
} else {
let (mut x, mut y, mut w, mut ang, mut op, mut flip) = (c.center.x, c.center.y, c.width, c.angle.to_degrees(), c.opacity, c.flip);
let mut changed = false;
changed |= ui_kit::row(ui, crate::i18n::tr_label("Center X"), |ui| ui_kit::length_field(ui, &mut x, &units, decimals)).changed();
changed |= ui_kit::row(ui, crate::i18n::tr_label("Center Y"), |ui| ui_kit::length_field(ui, &mut y, &units, decimals)).changed();
changed |= ui_kit::row(ui, crate::i18n::tr_label("Width"), |ui| ui_kit::length_field(ui, &mut w, &units, decimals)).changed();
changed |= ui_kit::row(ui, crate::i18n::tr_label("Angle"), |ui| ui_kit::angle_field(ui, &mut ang)).changed();
changed |=
ui_kit::row(ui, crate::i18n::tr_label("Opacity"), |ui| ui_kit::slider(ui, &mut op, 0.0..=1.0, Default::default())).changed();
if ui_kit::checkbox_row(ui, "flip", crate::i18n::tr_label("Flip"), flip) {
ui_kit::row(ui, solidcraft_ui_text::tr_label("Center X"), |ui| ui_kit::length_field(ui, &mut x, &units, decimals)).changed();
changed |=
ui_kit::row(ui, solidcraft_ui_text::tr_label("Center Y"), |ui| ui_kit::length_field(ui, &mut y, &units, decimals)).changed();
changed |= ui_kit::row(ui, solidcraft_ui_text::tr_label("Width"), |ui| ui_kit::length_field(ui, &mut w, &units, decimals)).changed();
changed |= ui_kit::row(ui, solidcraft_ui_text::tr_label("Angle"), |ui| ui_kit::angle_field(ui, &mut ang)).changed();
changed |= ui_kit::row(ui, solidcraft_ui_text::tr_label("Opacity"), |ui| ui_kit::slider(ui, &mut op, 0.0..=1.0, Default::default()))
.changed();
if ui_kit::checkbox_row(ui, "flip", solidcraft_ui_text::tr_label("Flip"), flip) {
flip = !flip;
changed = true;
}
@@ -113,11 +119,11 @@ pub(crate) fn click(app: &mut SolveApp, proj: &Proj, screen: Pos2) -> bool {
if panel.next == 0 {
panel.a = point;
panel.next = 1;
app.set_status(crate::i18n::tr("browser.calibrate_click_b"), false);
app.set_status(solidcraft_ui_text::tr("browser.calibrate_click_b"), false);
} else {
panel.b = point;
panel.next = 0;
app.set_status(crate::i18n::tr("browser.calibrate_click_a"), false);
app.set_status(solidcraft_ui_text::tr("browser.calibrate_click_a"), false);
}
app.tree.canvas_panel = Some(panel);
true
+7 -7
View File
@@ -1,7 +1,7 @@
use super::*;
use crate::scenario::{Harness, handle_at, rect_of};
use crate::semantic_content::{self, ContentOwner, ContentRequest};
use crate::shell::{LayoutMode, SHELL_GAP};
use crate::ui_kit::{LayoutMode, SHELL_GAP};
#[test]
fn clicked_browser_folders_toggle_visibility_with_v() {
@@ -318,7 +318,7 @@ fn selected_row_paints_tab_soft_fill_and_accent_text_in_both_layouts() {
let mut app = SolveApp::new(Default::default(), Default::default());
app.ui.layout = mode;
app.ui.tab = tab.into();
crate::theme::TabAccent::publish(&app, &ctx);
app.publish_tab_accent(&ctx);
let accent = crate::theme::TabAccent::for_tab(&Tokens::get(), tab);
let mut row_rect = Rect::NOTHING;
let out = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
@@ -595,7 +595,7 @@ fn browser_title_uses_a_single_semibold_run() {
ui.set_width(250.0);
browser_header(&mut app, ui, true);
});
let title = crate::i18n::tr("ui.browser_title");
let title = solidcraft_ui_text::tr("ui.browser_title");
let titles: Vec<_> = output
.shapes
.iter()
@@ -646,7 +646,7 @@ fn browser_width_rejects_hostile_preferences_and_tiny_windows_still_fit() {
assert_eq!(browser_width(-100.0), MIN_BROWSER_WIDTH);
assert_eq!(browser_width(f32::MAX), MAX_BROWSER_WIDTH);
let available = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 300.0));
for top_gap in [0.0, crate::shell::SHELL_GAP] {
for top_gap in [0.0, crate::ui_kit::SHELL_GAP] {
let bounds = panel_bounds(available, None, MAX_BROWSER_WIDTH, top_gap);
assert!(available.contains_rect(bounds));
}
@@ -677,7 +677,7 @@ fn collapsed_rail_shares_header_hover_inset_and_expands_from_its_whole_card() {
app.ui.tab = "SURFACE".into();
app.preferences.timeline_left = true;
app.preferences.timeline_left_browser_collapsed = collapsed;
crate::theme::TabAccent::publish(&app, &ctx);
app.publish_tab_accent(&ctx);
let bounds = Rect::from_min_size(Pos2::ZERO, vec2(600.0, 700.0));
let draw = |app: &mut SolveApp, events| {
crate::test_support::frame(&ctx, egui::RawInput { screen_rect: Some(bounds), events, ..Default::default() }, |ui| {
@@ -751,8 +751,8 @@ fn mesh_and_solid_body_rows_have_different_icons_and_tooltips() {
let row = |name: &str| rows.iter().find(|e| e.key == name).unwrap_or_else(|| panic!("no {name} row in {:?}", body_rows(&h.app)));
assert_eq!((row("Mesh").icon, row("Mesh").tip), ("mesh_body", Some("ui.body_kind_mesh")));
assert_eq!((row("Solid").icon, row("Solid").tip), ("body", Some("ui.body_kind_solid")));
assert_eq!(crate::i18n::tr("ui.body_kind_mesh"), "Mesh body");
assert_eq!(crate::i18n::tr("ui.body_kind_solid"), "Solid body");
assert_eq!(solidcraft_ui_text::tr("ui.body_kind_mesh"), "Mesh body");
assert_eq!(solidcraft_ui_text::tr("ui.body_kind_solid"), "Solid body");
// Its own drawing in the mesh colours, not the solid body's grey cube.
let icon = crate::icon_svg::resolve("mesh_body");
assert!(!icon.placeholder);
+2 -6
View File
@@ -211,11 +211,7 @@ fn with_rows_starting_closed_a_designs_folders_open_with_their_arrow_or_find_in_
"the Bodies folder stays closed"
);
crate::context_menu::run_item(
&mut h.app,
&crate::context_menu::Item::action("ui.findBrowser", "", "").with(json!({"body":"Block"})),
egui::Pos2::ZERO,
);
crate::context_menu::run_item(&mut h.app, &crate::ui_kit::Item::action("ui.findBrowser", "", "").with(json!({"body":"Block"})), egui::Pos2::ZERO);
h.frames(2);
assert!(super::is_open(&h.app, "c0/bodies", false), "Find in Browser opens the Bodies folder");
assert!(
@@ -571,7 +567,7 @@ fn feature_editor_browser_rows_follow_the_marker_and_return_after_cancel() {
fn the_edited_sketch_is_marked_in_the_users_language() {
let mut h = crate::scenario::Harness::new();
h.app.run("sketch.create", json!({"plane":"XY","name":"Sketch1"})).unwrap();
let _japanese = crate::i18n::scope("ja");
let _japanese = solidcraft_ui_text::scope("ja");
let rows = super::entries(&h.app, 0, "sketches");
assert_eq!(rows.iter().map(|r| r.label.as_str()).collect::<Vec<_>>(), ["スケッチ1 (編集中)"]);
}
+8 -8
View File
@@ -38,7 +38,7 @@ pub(crate) fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
let name = app.session.doc.occurrences.iter().find(|o| o.id == m.occurrence).map(|o| o.name.clone()).unwrap_or_default();
let before = (m.translate, m.angles);
let shared = !m.path.is_empty() && app.session.occurrence_scope_count(&m.path).is_ok_and(|n| n > 1);
let title = crate::i18n::format("browser.move_title", &[("name", &name)]);
let title = solidcraft_ui_text::format("browser.move_title", &[("name", &name)]);
let at = panel_pos(app);
let (units, decimals) = (app.session.doc.units.clone(), app.preferences.length_decimals);
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_occ_move"), "move", &title)
@@ -50,19 +50,19 @@ pub(crate) fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
if shared {
ui_kit::text(
ui,
crate::i18n::tr_label("This child is shared. Position changes apply to every copy of its parent."),
solidcraft_ui_text::tr_label("This child is shared. Position changes apply to every copy of its parent."),
ui_kit::Text::Note("shared"),
);
}
for (k, axis) in ["X distance", "Y distance", "Z distance"].into_iter().enumerate() {
if let Some(v) = m.translate.get_mut(k) {
let response = ui_kit::row(ui, crate::i18n::tr_label(axis), |ui| ui_kit::length_field(ui, v, &units, decimals));
let response = ui_kit::row(ui, solidcraft_ui_text::tr_label(axis), |ui| ui_kit::length_field(ui, v, &units, decimals));
crate::scenario::publish_handle(&format!("occurrence-move:distance:{k}"), response.rect.center());
}
}
for (k, axis) in ["X angle", "Y angle", "Z angle"].into_iter().enumerate() {
if let Some(v) = m.angles.get_mut(k) {
let response = ui_kit::row(ui, crate::i18n::tr_label(axis), |ui| ui_kit::angle_field(ui, v));
let response = ui_kit::row(ui, solidcraft_ui_text::tr_label(axis), |ui| ui_kit::angle_field(ui, v));
crate::scenario::publish_handle(&format!("occurrence-move:angle:{k}"), response.rect.center());
}
}
@@ -97,17 +97,17 @@ pub(crate) fn capture_prompt(app: &mut SolveApp, ctx: &egui::Context) {
return;
}
let n = app.session.pending_moves.len();
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_capture_prompt"), "capture", crate::i18n::tr_label("Capture Position"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_capture_prompt"), "capture", solidcraft_ui_text::tr_label("Capture Position"))
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, -80.0))
.actions("Capture Position", "Cancel", Some("Revert"))
.show(ctx, true, |ui| {
let question = if n == 1 {
crate::i18n::tr("browser.capture_one").to_string()
solidcraft_ui_text::tr("browser.capture_one").to_string()
} else {
crate::i18n::format("browser.capture_many", &[("count", &n.to_string())])
solidcraft_ui_text::format("browser.capture_many", &[("count", &n.to_string())])
};
ui_kit::text(ui, &question, ui_kit::Text::Report("question", false));
ui_kit::text(ui, crate::i18n::tr("browser.capture_note"), ui_kit::Text::Note("capture"));
ui_kit::text(ui, solidcraft_ui_text::tr("browser.capture_note"), ui_kit::Text::Note("capture"));
});
let answer = match action {
ui_kit::Action::Ok => Some("capture"),
+8 -7
View File
@@ -955,7 +955,7 @@ fn value_box_full(
let frame = Container::popup_frame(ui.style()).inner_margin(egui::Margin::symmetric(6, 3));
Container::show_backing(ui, frame, |ui| {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(crate::i18n::tr_label(label)).color(t.text_dim).size(crate::theme::Tokens::get().font_caption));
ui.label(egui::RichText::new(solidcraft_ui_text::tr_label(label)).color(t.text_dim).size(crate::theme::Tokens::get().font_caption));
let r = crate::ui_kit::canvas_value_field(ui, id, value, keep_tab);
crate::params_dialog::complete(ui, &r, value);
if select_all {
@@ -1049,7 +1049,7 @@ fn box_size(ui: &egui::Ui, area: egui::Id, label: &str, note: Option<&str>) -> e
let t = Tokens::get();
let font = egui::FontId::proportional(t.font_caption);
let width = |text: String| ui.painter().layout_no_wrap(text, font.clone(), t.text_dim).size().x;
let mut w = width(crate::i18n::tr_label(label).to_string()) + t.space_24 * 3.0;
let mut w = width(solidcraft_ui_text::tr_label(label).to_string()) + t.space_24 * 3.0;
let mut parts = 1;
if let Some(note) = note {
w += width(note.to_string());
@@ -1324,7 +1324,7 @@ fn radius_spline_handles(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Pain
let at = |u: f64| e.start + (e.other - e.start) * u;
let g = e.spline.greville();
let tips: Vec<Option<Pos2>> = g.iter().zip(&e.spline.radii).map(|(u, r)| proj.to_screen(at(*u) + e.n * *r)).collect();
painter.extend(crate::viewport::dashed_line(
painter.extend(crate::ui_kit::dashed_line(
&tips.iter().flatten().copied().collect::<Vec<_>>(),
painter.clip_rect(),
Stroke::new(1.0, t.manipulator),
@@ -1588,7 +1588,7 @@ fn measure_components(
let (at, offset) = if let Some((sa, sb)) = screen {
let leg = sb - sa;
if leg.length_sq() > 1.0 {
painter.extend(crate::viewport::dashed_line(&[sa, sb], painter.clip_rect(), Stroke::new(1.5, color), 5.0, 3.0));
painter.extend(crate::ui_kit::dashed_line(&[sa, sb], painter.clip_rect(), Stroke::new(1.5, color), 5.0, 3.0));
let normal = vec2(-leg.y, leg.x).normalized();
for p in [sa, sb] {
painter.line_segment([p - normal * 3.0, p + normal * 3.0], Stroke::new(1.0, color));
@@ -1709,7 +1709,7 @@ fn taper_handle(
crate::scenario::publish_handle("taper", es);
let hot = r.hovered() || r.dragged();
let col = if hot { t.accent } else { t.manipulator };
painter.extend(crate::viewport::dashed_line(&[fs, es], painter.clip_rect(), Stroke::new(1.2, col), 5.0, 3.0));
painter.extend(crate::ui_kit::dashed_line(&[fs, es], painter.clip_rect(), Stroke::new(1.2, col), 5.0, 3.0));
painter.circle(es, if hot { 6.5 } else { 5.0 }, t.overlay, Stroke::new(2.0, col));
if hot {
ui.ctx().set_cursor_icon(egui::CursorIcon::Grab);
@@ -1901,12 +1901,13 @@ fn draw_revolve(
} else {
"Choose target".into()
};
ui.label(text).backing_on_hover_text(crate::i18n::tr_label("Determined by To Object. Choose Angle to edit the angular extent."));
ui.label(text)
.backing_on_hover_text(solidcraft_ui_text::tr_label("Determined by To Object. Choose Angle to edit the angular extent."));
});
});
} else if let Kind::Revolve { angle, .. } = &mut d.kind {
let dragging = *angle != angle_before;
value_box(ui, crate::i18n::tr_label("Angle"), angle, box_at, focus || dragging, box_place(app, proj));
value_box(ui, solidcraft_ui_text::tr_label("Angle"), angle, box_at, focus || dragging, box_place(app, proj));
}
}
+1 -1
View File
@@ -265,7 +265,7 @@ fn analysis_shader_colours_come_from_the_uniform() {
"vec3<f32>(0.35)",
"vec3<f32>(1.0)",
],
"a new colour in the analysis shader must come from `ana_c` (theme::AnalysisColors)"
"a new colour in the analysis shader must come from `ana_c` (solidcraft_color::AnalysisColors)"
);
assert_eq!(body.matches("u.ana_c[").count(), 9, "every analysis colour reads `ana_c`");
}
+1 -2
View File
@@ -4,7 +4,6 @@
//! tooltip. Text only for now; pictures and animations are a later step. A new toolbar command
//! ships with its help: `every_toolbar_command_has_help` fails otherwise.
use crate::i18n;
use crate::ui_kit::BackingTooltipExt as _;
/// The commands that have help text. Each one needs a `help.<id>` message in the catalogs.
@@ -330,7 +329,7 @@ pub fn help(command: &str) -> Option<String> {
return None;
}
let key = format!("help.{command}");
let text = i18n::tr(&key);
let text = solidcraft_ui_text::tr(&key);
(text != key).then(|| text.to_owned())
}
+17 -16
View File
@@ -50,7 +50,7 @@ impl Default for Editor {
}
}
fn choose(ui: &mut Ui, id: &str, value: &mut String, options: &[String]) {
let mut labels = vec![crate::i18n::tr_label("Choose…")];
let mut labels = vec![solidcraft_ui_text::tr_label("Choose…")];
labels.extend(options.iter().map(String::as_str));
let mut selected = options.iter().position(|o| o == value).map_or(0, |i| i + 1);
let before = selected;
@@ -92,13 +92,13 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
let unlisted_features = suppression_features.len() < features.len();
let unlisted_parameters = all_parameters.iter().any(|p| !parameters.contains(p));
ui_kit::divider(ui, "configuration_rules");
ui_kit::text(ui, crate::i18n::tr_label("Configuration rules"), ui_kit::Text::Heading("rules_heading"));
ui_kit::text(ui, solidcraft_ui_text::tr_label("Configuration rules"), ui_kit::Text::Heading("rules_heading"));
ui_kit::text(
ui,
crate::i18n::tr_label("Rules run from top to bottom. Matched drives change effective values."),
solidcraft_ui_text::tr_label("Rules run from top to bottom. Matched drives change effective values."),
ui_kit::Text::Note("rules_drives"),
);
ui_kit::text(ui, crate::i18n::tr_label("Matched restrictions check the final values."), ui_kit::Text::Note("rules_restrictions"));
ui_kit::text(ui, solidcraft_ui_text::tr_label("Matched restrictions check the final values."), ui_kit::Text::Note("rules_restrictions"));
for (index, rule) in rules.iter().enumerate() {
ui.push_id(&rule.id, |ui| {
let summary = match &rule.effect {
@@ -114,21 +114,22 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
+ crate::theme::Tokens::get().space_4 * 3.0;
let mut enabled = rule.enabled;
if ui_kit::summary_row(ui, &rule.name, &summary, &mut enabled, actions_width, |ui| {
if ui_kit::button(ui, crate::i18n::tr_label("Edit"), ui_kit::Button::Normal).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr_label("Edit"), ui_kit::Button::Normal).clicked() {
editor.rule = rule.clone();
}
let up = ui_kit::button(ui, "↑", ui_kit::Button::Enabled(index > 0)).backing_on_hover_text(crate::i18n::tr_label("Move earlier"));
let up =
ui_kit::button(ui, "↑", ui_kit::Button::Enabled(index > 0)).backing_on_hover_text(solidcraft_ui_text::tr_label("Move earlier"));
crate::scenario::publish_handle(&format!("config:rule:{}:up", rule.id), up.rect.center());
if up.clicked() {
*error = app.run("config.rule", json!({"id":rule.id,"index":index-1})).err();
}
if ui_kit::button(ui, "↓", ui_kit::Button::Enabled(index + 1 < rules.len()))
.backing_on_hover_text(crate::i18n::tr_label("Move later"))
.backing_on_hover_text(solidcraft_ui_text::tr_label("Move later"))
.clicked()
{
*error = app.run("config.rule", json!({"id":rule.id,"index":index+1})).err();
}
if ui_kit::button(ui, crate::i18n::tr_label("Delete"), ui_kit::Button::Normal).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr_label("Delete"), ui_kit::Button::Normal).clicked() {
*error = app.run("config.rule", json!({"id":rule.id,"delete":true})).err();
}
}) {
@@ -141,8 +142,8 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
ui_kit::section(ui, "rule_editor", "Add or edit rule", false, |ui| {
ui.vertical(|ui| {
expression(ui, "Rule name", &mut editor.rule.name);
ui_kit::checkbox(ui, crate::i18n::tr_label("Enabled"), &mut editor.rule.enabled);
if ui_kit::button(ui, crate::i18n::tr_label("New"), ui_kit::Button::Small).clicked() {
ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Enabled"), &mut editor.rule.enabled);
if ui_kit::button(ui, solidcraft_ui_text::tr_label("New"), ui_kit::Button::Small).clicked() {
editor.rule = Editor::default().rule;
}
});
@@ -192,7 +193,7 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
ui.vertical(|ui| {
feature_choice(ui, "effect_feature", feature, &suppression_features);
columns_note(ui, unlisted_features, "config.rule_suppression_columns");
ui_kit::checkbox(ui, crate::i18n::tr_label("Suppressed"), value);
ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Suppressed"), value);
});
}
RuleEffect::ParameterRange { parameter, minimum, maximum } => {
@@ -258,7 +259,7 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
}
if let Some(preview) = &editor.preview {
ui_kit::section(ui, "rule_preview", &format!("Preview: {}", preview["active"].as_str().unwrap_or("row")), false, |ui| {
report(ui, crate::i18n::tr_label("Preview leaves the current design unchanged."));
report(ui, solidcraft_ui_text::tr_label("Preview leaves the current design unchanged."));
if let Some(styles) = preview["styles"].as_array() {
for style in styles {
report(
@@ -326,11 +327,11 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
/// how to add one.
fn columns_note(ui: &mut Ui, unlisted: bool, message: &'static str) {
if unlisted {
ui_kit::text(ui, crate::i18n::tr(message), ui_kit::Text::Note(message));
ui_kit::text(ui, solidcraft_ui_text::tr(message), ui_kit::Text::Note(message));
}
}
fn report(ui: &mut Ui, text: impl AsRef<str>) {
ui_kit::text(ui, crate::i18n::tr_label(text.as_ref()), ui_kit::Text::Report("rule_report", false));
ui_kit::text(ui, solidcraft_ui_text::tr_label(text.as_ref()), ui_kit::Text::Report("rule_report", false));
}
fn feature_choice(ui: &mut Ui, id: &str, feature: &mut u64, features: &[(u64, String)]) {
@@ -398,7 +399,7 @@ fn condition_editor(ui: &mut Ui, condition: &mut RuleCondition, parameters: &[St
RuleCondition::Suppression { feature, value } => {
ui.vertical(|ui| {
feature_choice(ui, "condition_feature", feature, features);
ui_kit::checkbox(ui, crate::i18n::tr_label("Suppressed"), value);
ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Suppressed"), value);
});
}
RuleCondition::All { conditions } | RuleCondition::Any { conditions } => {
@@ -407,7 +408,7 @@ fn condition_editor(ui: &mut Ui, condition: &mut RuleCondition, parameters: &[St
ui.push_id(index, |ui| {
ui.indent("condition_group", |ui| {
condition_editor(ui, child, parameters, features, depth + 1);
if ui_kit::button(ui, crate::i18n::tr_label("Remove condition"), ui_kit::Button::Small).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr_label("Remove condition"), ui_kit::Button::Small).clicked() {
remove = Some(index);
}
});
+12 -12
View File
@@ -26,7 +26,7 @@ pub(super) fn configurations_rows(
let run = |app: &mut SolveApp, error: &mut Option<String>, id: &str, p: Value| {
*error = app.run(id, p).err();
};
let labels: Vec<_> = std::iter::once(crate::i18n::tr_label("Configuration").to_owned())
let labels: Vec<_> = std::iter::once(solidcraft_ui_text::tr_label("Configuration").to_owned())
.chain(tb.columns.iter().map(|c| {
let (kind, name) = column_parts(c);
v["targets"]
@@ -36,13 +36,13 @@ pub(super) fn configurations_rows(
.filter(|_| matches!(kind, "component_appearance" | "face_appearance"))
.map(str::to_string)
.unwrap_or_else(|| match kind {
"material" => format!("{}: {name}", crate::i18n::tr_label("Material")),
"appearance" => format!("{}: {name}", crate::i18n::tr_label("Appearance")),
"suppress" => format!("{}: {name}", crate::i18n::tr_label("Suppress")),
"material" => format!("{}: {name}", solidcraft_ui_text::tr_label("Material")),
"appearance" => format!("{}: {name}", solidcraft_ui_text::tr_label("Appearance")),
"suppress" => format!("{}: {name}", solidcraft_ui_text::tr_label("Suppress")),
_ => name.to_owned(),
})
}))
.chain(std::iter::once(crate::i18n::tr_label("Actions").to_owned()))
.chain(std::iter::once(solidcraft_ui_text::tr_label("Actions").to_owned()))
.collect();
let gap = ui.spacing().item_spacing.x;
let name_width = tb.rows.iter().map(|(name, _)| ui_kit::ContentWidth::name(ui, name)).fold(0.0, f32::max);
@@ -76,7 +76,7 @@ pub(super) fn configurations_rows(
}
})
.collect();
ui_kit::table_custom(ui, "configuration_table", &columns, crate::i18n::tr("manage.empty"), |ui, table| {
ui_kit::table_custom(ui, "configuration_table", &columns, solidcraft_ui_text::tr("manage.empty"), |ui, table| {
table.headers(ui, |ui, index| {
if index == labels.len() - 1 {
column_menu(app, ui, error, &tb.columns, rule_editor);
@@ -112,14 +112,14 @@ pub(super) fn configurations_rows(
if active {
ui_kit::active_pill(ui);
if v.get("matches").and_then(Value::as_bool) == Some(false) {
let apply = ui_kit::button(ui, crate::i18n::tr_label("Apply"), ui_kit::Button::Small);
let apply = ui_kit::button(ui, solidcraft_ui_text::tr_label("Apply"), ui_kit::Button::Small);
crate::scenario::publish_handle(&format!("config:activate:{name}"), apply.rect.center());
if apply.clicked() {
run(app, error, "config.activate", json!({"row":name}));
}
}
} else {
let activate = ui_kit::button(ui, crate::i18n::tr_label("Activate"), ui_kit::Button::Small);
let activate = ui_kit::button(ui, solidcraft_ui_text::tr_label("Activate"), ui_kit::Button::Small);
crate::scenario::publish_handle(&format!("config:activate:{name}"), activate.rect.center());
if activate.clicked() {
run(app, error, "config.activate", json!({"row":name}));
@@ -343,7 +343,7 @@ fn column_menu(
let mut pick: Option<Value> = None;
let filter = &mut rule_editor.column_filter;
let was_open = rule_editor.column_menu_open;
let face_appearance = crate::i18n::tr_label("Face Appearance");
let face_appearance = solidcraft_ui_text::tr_label("Face Appearance");
let shown = ui_kit::plus_header(ui, |ui| {
// A design has many parameters, features and bodies: a filter narrows the list as the
// Parameters dialog's does. It takes focus as the menu opens, so typing filters at once.
@@ -361,7 +361,7 @@ fn column_menu(
}
let faces = query.is_empty() || face_appearance.to_lowercase().contains(&query);
if matching.is_empty() && !faces {
ui_kit::text(ui, crate::i18n::tr("config.column_filter_empty"), ui_kit::Text::Note("column_filter_empty"));
ui_kit::text(ui, solidcraft_ui_text::tr("config.column_filter_empty"), ui_kit::Text::Note("column_filter_empty"));
}
// The popup remembers its size: once the filter has emptied the list, a list that fills
// again would be clipped to that small height, so a list that scrolls takes the full one.
@@ -379,7 +379,7 @@ fn column_menu(
ui_kit::action_menu(ui, "Face Appearance", |ui| {
ui_kit::text(
ui,
crate::i18n::tr_label("Semantic faces share their appearance across instances."),
solidcraft_ui_text::tr_label("Semantic faces share their appearance across instances."),
ui_kit::Text::Note("semantic_faces"),
);
let state = app.session.model.state();
@@ -486,7 +486,7 @@ fn value_control(ui: &mut egui::Ui, id: egui::Id, text: &mut String, kind: &str,
"Default material"
};
ui_kit::library_cell(ui, id, text, follow, |ui| {
if ui_kit::button(ui, crate::i18n::tr_label(follow), ui_kit::Button::Normal).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr_label(follow), ui_kit::Button::Normal).clicked() {
// An appearance's "Default" keyword follows; a material's is the default material.
let follow_value = if appearance { "Default" } else { DEFAULT_MATERIAL };
choice = Some(follow_value.to_owned());
+11 -103
View File
@@ -11,7 +11,7 @@
//! state; [`items`] lists them and [`run_item`] runs one, so the control channel (`ui.menu`,
//! `ui.menuPick`) and tests reach everything a click does.
use crate::ui_kit::{BackingTooltipExt as _, Container, Surface};
use crate::ui_kit::{BackingTooltipExt as _, Container, Item, LIST_W, Surface};
use egui::{Color32, Pos2, Rect, Sense, Vec2, pos2, vec2};
use serde::Serialize;
use serde_json::{Value, json};
@@ -127,88 +127,6 @@ pub enum Target {
},
}
/// One menu entry: a command id (run with `params` when given, else started like a toolbar
/// click) or a `ui.*` action. `-` is a separator, `#…` a section heading.
#[derive(Clone, Debug, Serialize)]
pub struct Item {
pub id: String,
pub label: String,
pub shortcut: String,
pub enabled: bool,
pub icon: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<Item>,
#[serde(skip_serializing_if = "Option::is_none")]
pub swatch: Option<[u8; 3]>,
/// Why the entry is off: shown on hover, and in the control channel's report.
#[serde(skip_serializing_if = "Option::is_none")]
pub tip: Option<String>,
}
impl Item {
/// A view action (`ui.*`).
pub fn action(id: &str, label: &str, icon: &str) -> Item {
act(id, label, icon)
}
pub fn sep() -> Item {
Item {
id: "-".into(),
label: String::new(),
shortcut: String::new(),
enabled: false,
icon: String::new(),
params: None,
children: Vec::new(),
swatch: None,
tip: None,
}
}
pub(crate) fn heading(label: &str) -> Item {
Item {
id: format!("#{label}"),
label: label.into(),
shortcut: String::new(),
enabled: false,
icon: String::new(),
params: None,
children: Vec::new(),
swatch: None,
tip: None,
}
}
fn is_sep(&self) -> bool {
self.id == "-"
}
fn is_heading(&self) -> bool {
self.id.starts_with('#')
}
pub fn with(mut self, params: Value) -> Item {
self.params = Some(params);
self
}
pub fn on_if(self, enabled: bool) -> Item {
self.on(enabled)
}
pub(crate) fn on(mut self, enabled: bool) -> Item {
self.enabled &= enabled;
self
}
/// Off, saying why, when there is a reason.
pub(crate) fn off_because(mut self, why: Option<String>) -> Item {
if let Some(why) = why {
self.enabled = false;
self.tip = Some(why);
}
self
}
pub fn key(mut self, k: &str) -> Item {
self.shortcut = k.into();
self
}
}
/// A command entry: label, icon, shortcut and enablement come from the registry.
pub(crate) fn cmd(app: &SolveApp, id: &str, label: &str) -> Item {
let info = solidcraft_engine::find_command(id).map(|c| c.info(&app.session));
@@ -232,17 +150,7 @@ pub(crate) fn cmd(app: &SolveApp, id: &str, label: &str) -> Item {
/// A view action (`ui.*`).
pub(crate) fn act(id: &str, label: &str, icon: &str) -> Item {
Item {
id: id.into(),
label: label.into(),
shortcut: String::new(),
enabled: true,
icon: icon.into(),
params: None,
children: Vec::new(),
swatch: None,
tip: None,
}
Item::action(id, label, icon)
}
/// Menu state kept between frames. The anchor is `app.viewport.context_menu`.
@@ -1701,7 +1609,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
if inserted.is_ok() {
app.fit_view();
if p.get("unsaved").and_then(Value::as_bool) == Some(true) {
app.set_status(crate::i18n::format("ui.insert_design.saved_file", &[("name", name)]), false);
app.set_status(solidcraft_ui_text::format("ui.insert_design.saved_file", &[("name", name)]), false);
}
}
}
@@ -2015,7 +1923,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
let calibrate = item.id == "ui.calibrateCanvas";
app.tree.canvas_panel = Some(crate::browser::CanvasPanel::new(app, c, calibrate));
if calibrate {
app.set_status(crate::i18n::tr("browser.calibrate_click_a"), false);
app.set_status(solidcraft_ui_text::tr("browser.calibrate_click_a"), false);
}
}
}
@@ -2048,15 +1956,18 @@ fn sketches_to_parts(app: &mut SolveApp, p: Value) {
let mut why: Vec<String> = Vec::new();
for kept in out.get("kept").and_then(Value::as_array).into_iter().flatten() {
let code = kept.get("code").and_then(Value::as_str).unwrap_or_default();
let reason = crate::i18n::tr(&format!("browser.sketches_to_parts_{code}")).to_string();
let reason = solidcraft_ui_text::tr(&format!("browser.sketches_to_parts_{code}")).to_string();
if !why.contains(&reason) {
why.push(reason);
}
}
let message = if why.is_empty() {
crate::i18n::format("browser.sketches_to_parts_moved", &[("moved", &count("moved"))])
solidcraft_ui_text::format("browser.sketches_to_parts_moved", &[("moved", &count("moved"))])
} else {
crate::i18n::format("browser.sketches_to_parts_result", &[("moved", &count("moved")), ("stayed", &count("stayed")), ("why", &why.join("; "))])
solidcraft_ui_text::format(
"browser.sketches_to_parts_result",
&[("moved", &count("moved")), ("stayed", &count("stayed")), ("why", &why.join("; "))],
)
};
app.set_status(message, false);
}
@@ -2535,9 +2446,6 @@ fn list_place(c: Pos2, h: f32, screen: Rect) -> Pos2 {
pos2((c.x - RX - 12.0 - LIST_W).max(screen.left() + 4.0), y)
}
/// Width of list menus.
pub const LIST_W: f32 = 250.0;
/// Where a menu of `size` opened at `at` goes: right of and below the point (`gap` away) when it
/// fits in `screen`, flipped to the left or above it when it would run past that edge, and
/// against the far edge when it fits on neither side. A flipped menu ends at the cursor, so it
@@ -3405,7 +3313,7 @@ mod tests {
assert!(h.app.cam.back().dist(flat.plane_normal.unwrap()) < 2e-6);
assert_eq!((h.app.cam.distance, h.app.cam.fov), (before.distance, before.fov));
assert!(h.app.viewport.context_menu.is_none());
assert_eq!(crate::i18n::Locale::for_language("ja").label("Normal to Selection"), "選択アイテムに垂直");
assert_eq!(solidcraft_ui_text::Locale::for_language("ja").label("Normal to Selection"), "選択アイテムに垂直");
// Ctrl+8 and the context menu share Look At, including its repeat-to-flip behavior.
h.step(&json!({"key":"8","cmd":true})).unwrap();
h.frames(90);
+2 -2
View File
@@ -182,7 +182,7 @@ impl ControlJob {
}
fn label(app: &SolveApp, command: &str) -> String {
let language = crate::i18n::Locale::for_language(&app.preferences.language);
let language = solidcraft_ui_text::Locale::for_language(&app.preferences.language);
language.label(solidcraft_engine::find_command(command).map_or(command, |c| c.label)).to_string()
}
@@ -473,7 +473,7 @@ impl SolveApp {
let reply = reply.unwrap_or_else(|| json!({"ok": false, "error": format!("`{}` ended without a reply", job.label)}));
let export = executed(&job.req.params).and_then(|(command, _)| spec_id(command)).is_some_and(|id| ANSWERED_ON_A_COPY.contains(&id));
if reply["ok"] == true && export {
let language = crate::i18n::Locale::for_language(&self.preferences.language);
let language = solidcraft_ui_text::Locale::for_language(&self.preferences.language);
self.session.echo(language.format("ui.command_done", &[("command", job.label.as_str())]));
}
let _ = job.req.reply.send(reply);
+21 -17
View File
@@ -37,7 +37,7 @@ impl CrashUi {
}
fn kind_label(kind: &str) -> &'static str {
crate::i18n::tr(match kind {
solidcraft_ui_text::tr(match kind {
"command_panic" => "crash.kind_command",
"kernel_panic_recovered" => "crash.kind_kernel",
"unclean_exit" => "crash.kind_unclean",
@@ -48,7 +48,7 @@ fn kind_label(kind: &str) -> &'static str {
fn report_time(r: &Summary) -> String {
let time = r.time.replace('T', " ");
let time = time.trim_end_matches('Z');
if r.kind == "unclean_exit" { crate::i18n::format("crash.detected_time", &[("time", time)]) } else { format!("{time} UTC") }
if r.kind == "unclean_exit" { solidcraft_ui_text::format("crash.detected_time", &[("time", time)]) } else { format!("{time} UTC") }
}
fn report_heading(r: &Summary) -> String {
@@ -66,7 +66,7 @@ pub fn announce(app: &mut SolveApp, fresh: &[Summary]) {
}
app.crashes.open = true;
app.crashes.refresh();
let locale = crate::i18n::Locale::for_language(&app.preferences.language);
let locale = solidcraft_ui_text::Locale::for_language(&app.preferences.language);
app.set_status(locale.text("crash.previous_session"), true);
}
@@ -76,17 +76,17 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
let mut report: Option<String> = None;
let mut delete: Option<Option<String>> = None;
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_crash_reports"), "bug_report", crate::i18n::tr_label("Crash Reports"))
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_crash_reports"), "bug_report", solidcraft_ui_text::tr_label("Crash Reports"))
.cards(2.0)
.actions("", crate::i18n::tr("ui.close"), None)
.actions("", solidcraft_ui_text::tr("ui.close"), None)
.show(ctx, true, |ui| {
ui.spacing_mut().item_spacing.y = crate::theme::Tokens::get().space_10;
let intro = ui_kit::text(ui, crate::i18n::tr("crash.intro"), ui_kit::Text::Hint("intro"));
let intro = ui_kit::text(ui, solidcraft_ui_text::tr("crash.intro"), ui_kit::Text::Hint("intro"));
crate::scenario::publish_rect("dialog:note:intro", intro.rect);
if let Some(d) = app.crashes.dir.clone() {
// A long path is cut short (hover shows it all): it must not widen the card.
ui_kit::path_chip(ui, &d, |ui| {
if ui_kit::icon_label_button(ui, crate::i18n::tr("crash.copy_folder"), "copy", true, false).clicked() {
if ui_kit::icon_label_button(ui, solidcraft_ui_text::tr("crash.copy_folder"), "copy", true, false).clicked() {
ctx.copy_text(d.clone());
}
});
@@ -97,7 +97,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
egui::Rect::from_min_size(ui.next_widget_position(), egui::vec2(ui.available_width(), 1.0)),
);
if app.crashes.reports.is_empty() {
ui_kit::empty_box(ui, "none", crate::i18n::tr_label("No crash reports."));
ui_kit::empty_box(ui, "none", solidcraft_ui_text::tr_label("No crash reports."));
}
if !app.crashes.reports.is_empty() {
ui_kit::report_list(
@@ -112,27 +112,31 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
ui.spacing_mut().item_spacing.y = crate::theme::Tokens::get().space_6;
ui.add_space(crate::theme::Tokens::get().space_10);
ui_kit::report_header(ui, kind_label(&r.kind), r.count, &report_time(r), &title, |ui| {
if ui_kit::bin_button(ui, crate::i18n::tr("ui.delete")).clicked() {
if ui_kit::bin_button(ui, solidcraft_ui_text::tr("ui.delete")).clicked() {
delete = Some(Some(r.id.clone()));
}
ui_kit::icon_label_button(ui, crate::i18n::tr("crash.copy_path"), "copy", true, false)
ui_kit::icon_label_button(ui, solidcraft_ui_text::tr("crash.copy_path"), "copy", true, false)
.clicked()
.then(|| ctx.copy_text(r.path.clone()));
if ui_kit::icon_label_button(ui, crate::i18n::tr("crash.report"), "bug_report", true, false).clicked() {
if ui_kit::icon_label_button(ui, solidcraft_ui_text::tr("crash.report"), "bug_report", true, false).clicked() {
report = Some(r.id.clone());
}
});
let mut detail = r.command.clone().unwrap_or_default();
if r.replayable {
let sep = if detail.is_empty() { "" } else { " · " };
detail = format!("{detail}{sep}{}", crate::i18n::tr("crash.replayable"));
detail = format!("{detail}{sep}{}", solidcraft_ui_text::tr("crash.replayable"));
}
if !detail.is_empty() {
ui_kit::text(ui, &detail, ui_kit::Text::MonoNote(&format!("detail:{}", r.id)));
}
ui_kit::text(ui, &r.message, ui_kit::Text::Report(&format!("message:{}", r.id), false));
if r.kind == "unclean_exit" {
ui_kit::text(ui, crate::i18n::tr("crash.unclean_explanation"), ui_kit::Text::Note(&format!("unclean:{}", r.id)));
ui_kit::text(
ui,
solidcraft_ui_text::tr("crash.unclean_explanation"),
ui_kit::Text::Note(&format!("unclean:{}", r.id)),
);
}
ui.add_space(crate::theme::Tokens::get().space_6);
if index != last {
@@ -146,7 +150,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if !app.crashes.note.is_empty() {
ui_kit::text(ui, &app.crashes.note, ui_kit::Text::Note("note"));
}
if ui_kit::icon_label_button(ui, crate::i18n::tr_label("Delete all"), "delete", !app.crashes.reports.is_empty(), true).clicked() {
if ui_kit::icon_label_button(ui, solidcraft_ui_text::tr_label("Delete all"), "delete", !app.crashes.reports.is_empty(), true).clicked() {
delete = Some(None);
}
});
@@ -163,7 +167,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
&& let Some(dir) = app.crashes.dir_path()
{
let n = crash::clear(&dir, which.as_deref());
app.crashes.note = crate::i18n::format("crash.deleted", &[("count", &n.to_string())]);
app.crashes.note = solidcraft_ui_text::format("crash.deleted", &[("count", &n.to_string())]);
app.crashes.refresh();
}
}
@@ -218,7 +222,7 @@ mod tests {
assert!(status.contains("detected at startup"), "{status}");
assert!(h.widget("Detected 2026-10-12 16:23:41 UTC Closed unexpectedly ×2").is_some());
assert!(h.widget(report["message"].as_str().unwrap()).is_some(), "keep the original session start visible");
assert!(h.widget(crate::i18n::tr("crash.unclean_explanation")).is_some());
assert!(h.widget(solidcraft_ui_text::tr("crash.unclean_explanation")).is_some());
let _ = std::fs::remove_dir_all(&d);
}
@@ -236,7 +240,7 @@ mod tests {
h.frames(6);
assert!(h.app.crashes.open);
assert!(h.widget("2026-10-12 16:23:41 UTC Internal error in a command ×2").is_some());
assert!(h.widget(crate::i18n::tr("crash.unclean_explanation")).is_none());
assert!(h.widget(solidcraft_ui_text::tr("crash.unclean_explanation")).is_none());
let _ = std::fs::remove_dir_all(&d);
}
+4 -4
View File
@@ -185,14 +185,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
};
let (deleted, fails) = (list("deleted"), list("would_fail"));
let shared = shared_occurrences(app, &p.params);
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_delete_confirm"), "delete", crate::i18n::tr_label("Delete"))
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_delete_confirm"), "delete", solidcraft_ui_text::tr_label("Delete"))
.tone(ui_kit::Tone::Danger)
.actions("Delete", "Cancel", None)
.show(ctx, true, |ui| {
if shared {
ui_kit::text(
ui,
crate::i18n::tr_label("This child is shared. Deleting it removes it from every copy of its parent."),
solidcraft_ui_text::tr_label("This child is shared. Deleting it removes it from every copy of its parent."),
ui_kit::Text::Report("shared", false),
);
}
@@ -202,7 +202,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
if names.is_empty() {
continue;
}
ui_kit::text(ui, crate::i18n::tr_label(heading), ui_kit::Text::Report(key, error));
ui_kit::text(ui, solidcraft_ui_text::tr_label(heading), ui_kit::Text::Report(key, error));
for name in names.iter().take(MAX_LISTED) {
ui_kit::text(ui, &format!(" • {name}"), ui_kit::Text::Report(&format!("{key}:{name}"), error));
}
@@ -210,7 +210,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let more = (names.len() - MAX_LISTED).to_string();
ui_kit::text(
ui,
&crate::i18n::format("delete.and_more", &[("count", &more)]),
&solidcraft_ui_text::format("delete.and_more", &[("count", &more)]),
ui_kit::Text::Report(&format!("{key}:more"), error),
);
}
+16 -14
View File
@@ -99,10 +99,10 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
d.refused = None;
}
let metadata = app.session.doc.metadata.clone();
let title = crate::i18n::tr_label("Design Properties");
let title = solidcraft_ui_text::tr_label("Design Properties");
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_design_properties"), "settings", title).fields().show(ctx, true, |ui| {
for (i, k) in keys(Owner::Design).enumerate() {
let (key, label) = (k.name, crate::i18n::tr(k.label));
let (key, label) = (k.name, solidcraft_ui_text::tr(k.label));
let Some(field) = d.fields.get_mut(i) else { continue };
if key == DESCRIPTION {
ui_kit::multiline_text_field(ui, label, egui::Id::unique(("sc_design_properties", key)), field);
@@ -111,15 +111,15 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
// Saved by another tool as something other than text: kept unless typed over.
if metadata.get(key).is_some() && metadata.text(key).is_none() && d.opened.get(i).is_some_and(String::is_empty) {
ui_kit::caption(ui, label, crate::i18n::tr("ui.design_properties.not_text"));
ui_kit::caption(ui, label, solidcraft_ui_text::tr("ui.design_properties.not_text"));
}
}
ui_kit::text(ui, crate::i18n::tr("ui.design_properties.exports"), ui_kit::Text::Note("exports"));
ui_kit::text(ui, solidcraft_ui_text::tr("ui.design_properties.exports"), ui_kit::Text::Note("exports"));
let others: Vec<(&String, &Value)> = metadata.others().collect();
if !others.is_empty() {
let heading = format!("{} ({})", crate::i18n::tr("ui.design_properties.others"), others.len());
let heading = format!("{} ({})", solidcraft_ui_text::tr("ui.design_properties.others"), others.len());
ui_kit::section(ui, "sc_design_properties_others", &heading, false, |ui| {
ui_kit::text(ui, crate::i18n::tr("ui.design_properties.others_note"), ui_kit::Text::Note("others"));
ui_kit::text(ui, solidcraft_ui_text::tr("ui.design_properties.others_note"), ui_kit::Text::Note("others"));
egui::ScrollArea::vertical().max_height(ui_kit::row_height() * OTHERS_ROWS).show(ui, |ui| {
for (key, value) in others {
ui_kit::row(ui, key, |ui| ui_kit::text(ui, &preview(value), ui_kit::Text::Cell));
@@ -263,7 +263,9 @@ fn show_measuring(app: &mut SolveApp, ctx: &egui::Context) {
let bodies = m.bodies();
let heading = app.tr("ui.properties").to_string();
let text = match bodies {
Some((done, total)) => crate::i18n::format("ui.properties.measuring_bodies", &[("done", &done.to_string()), ("total", &total.to_string())]),
Some((done, total)) => {
solidcraft_ui_text::format("ui.properties.measuring_bodies", &[("done", &done.to_string()), ("total", &total.to_string())])
}
None => app.tr("ui.properties.measuring").to_string(),
};
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_props"), "settings", &heading).actions("", "Close", None).show(ctx, true, |ui| {
@@ -344,7 +346,7 @@ fn inertia_rows(app: &SolveApp, total: &Value) -> Vec<(String, Vec<String>)> {
}
for (n, axis) in total["principal_axes"].as_array().into_iter().flatten().enumerate() {
let Some(e) = point(axis) else { continue };
let label = crate::i18n::format("ui.properties.principal_axis", &[("number", &(n + 1).to_string())]);
let label = solidcraft_ui_text::format("ui.properties.principal_axis", &[("number", &(n + 1).to_string())]);
rows.push((label, [e.x, e.y, e.z].map(|x| format!("{:.4}", if x.abs() < 5e-5 { 0.0 } else { x })).to_vec()));
}
rows
@@ -354,9 +356,9 @@ fn inertia_rows(app: &SolveApp, total: &Value) -> Vec<(String, Vec<String>)> {
/// file's), else with the name of the component, placement or bodies it measured.
fn window_title(title: &str, v: &Value) -> String {
if v["component"].as_u64() == Some(0) {
crate::i18n::tr("ui.properties").to_string()
solidcraft_ui_text::tr("ui.properties").to_string()
} else {
crate::i18n::format("properties.title", &[("name", title)])
solidcraft_ui_text::format("properties.title", &[("name", title)])
}
}
@@ -406,7 +408,7 @@ pub fn show_object(app: &mut SolveApp, ctx: &egui::Context) {
rows.push(("Density", format!("{:.3} g/cm³", number(b, "density_g_cm3"))));
}
if !bodies.is_empty() && skipped == bodies.len() as u64 {
rows.push(("", crate::i18n::tr_label("Open surface — no volume").to_string()));
rows.push(("", solidcraft_ui_text::tr_label("Open surface — no volume").to_string()));
} else {
rows.push(("Mass", format!("{:.3} g", number(total, "mass_g"))));
rows.push(("Volume", crate::prefs::show_mm(app, number(total, "volume_mm3"), 3)));
@@ -436,7 +438,7 @@ pub fn show_object(app: &mut SolveApp, ctx: &egui::Context) {
format!("({}) g·{unit}", m.join(", "))
});
let inertia = inertia_rows(app, total);
let note = crate::i18n::format("ui.properties.inertia_note", &[("unit", unit)]);
let note = solidcraft_ui_text::format("ui.properties.inertia_note", &[("unit", unit)]);
// A component's closed bodies have mass, but a mesh that would not close gave no tensor.
let unavailable =
inertia.is_empty() && total["inertia_g_mm2"].is_null() && number(total, "mass_g") > 0.0 && total.get("first_moment_g_mm").is_some();
@@ -450,7 +452,7 @@ pub fn show_object(app: &mut SolveApp, ctx: &egui::Context) {
let heading = window_title(&title, &v);
let (closed, action) = ui_kit::Window::new(egui::Id::unique("sc_props"), "settings", &heading).actions("", "Close", None).show(ctx, true, |ui| {
for (label, value) in &rows {
ui_kit::row(ui, crate::i18n::tr_label(label), |ui| ui_kit::text(ui, value, ui_kit::Text::Cell));
ui_kit::row(ui, solidcraft_ui_text::tr_label(label), |ui| ui_kit::text(ui, value, ui_kit::Text::Cell));
}
if let Some(first) = &first {
ui_kit::row(ui, &first_label, |ui| ui_kit::text(ui, first, ui_kit::Text::Cell));
@@ -591,7 +593,7 @@ mod tests {
#[test]
fn each_field_is_labelled_from_the_table_of_keys() {
use solidcraft_engine::doc::metadata::{KEYS, Owner, keys};
let english = crate::i18n::Locale::for_language("en");
let english = solidcraft_ui_text::Locale::for_language("en");
for k in KEYS {
assert_ne!(english.text(k.label), k.label, "`{}`'s label is in the English catalog", k.name);
}
+32 -28
View File
@@ -2558,7 +2558,7 @@ pub(crate) fn combo(ui: &mut egui::Ui, id: &str, labels: &[&str], sel: &mut usiz
}
fn tr(text: &str) -> &str {
crate::i18n::tr_label(text)
solidcraft_ui_text::tr_label(text)
}
fn publish_check(ui: &egui::Ui, response: &egui::Response, selected: bool) {
@@ -2755,7 +2755,8 @@ fn object_type_row(d: &mut Dialog, ui: &mut egui::Ui) {
let cur = usize::from(inp.accept == selection::FEATURES);
let mut ty = cur;
// What the two kinds are, and that a sketch is neither (#10057).
choice_row(ui, "Object Type", "pat_obj", &mut ty, &["Bodies", "Features"]).backing_on_hover_text(crate::i18n::tr("ui.pattern_object_type_tip"));
choice_row(ui, "Object Type", "pat_obj", &mut ty, &["Bodies", "Features"])
.backing_on_hover_text(solidcraft_ui_text::tr("ui.pattern_object_type_tip"));
if ty != cur {
inp.accept = if ty == 1 { selection::FEATURES } else { BODIES };
inp.label = objects_label(inp.accept);
@@ -2927,25 +2928,27 @@ fn occurrence_paths(s: &Session, items: &[Sel]) -> Vec<Vec<u64>> {
/// the name the preferences give it.
pub(crate) fn selection_label(s: &Session, names: crate::prefs::OriginPlaneNames, d: &Dialog, inp: &SelInput, item: &Sel) -> String {
let axis = inp.accept == selection::AXIS_PICKS && round_axis(s, item).is_some();
let sketch_name =
|id| s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_else(|| crate::i18n::format("browser.sketch_name", &[("number", &id.to_string())]));
let sketch_name = |id| {
s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_else(|| solidcraft_ui_text::format("browser.sketch_name", &[("number", &id.to_string())]))
};
match item {
Sel::SketchCurve { id, .. } => id.clone(),
Sel::SketchPoint { id } => crate::sketch_modify_ui::point_id(s, item).unwrap_or(id).to_string(),
Sel::SketchMidpoint { id } => crate::i18n::format("dialog.selection_midpoint", &[("id", id)]),
Sel::SketchMidpoint { id } => solidcraft_ui_text::format("dialog.selection_midpoint", &[("id", id)]),
Sel::ExternalSketchCurve { sketch, id } => {
crate::i18n::format("dialog.selection_external_curve", &[("sketch", &sketch_name(*sketch)), ("id", id)])
solidcraft_ui_text::format("dialog.selection_external_curve", &[("sketch", &sketch_name(*sketch)), ("id", id)])
}
// Another sketch's point (a Sphere's centre, #9403), named as Select Other names it.
Sel::ExternalSketchPoint { sketch, id } => {
let state = crate::open_meshing::session_world(s);
let point = state.sketch(*sketch).map_or_else(|| id.clone(), |ss| crate::select_other::point_label(&ss.sketch, id));
crate::i18n::format("dialog.selection_external_point", &[("point", &point), ("sketch", &sketch_name(*sketch))])
}
Sel::SketchConstraint { id } => crate::i18n::format("dialog.selection_constraint", &[("id", id)]),
Sel::Profile { sketch, index } => {
crate::i18n::format("dialog.selection_profile", &[("sketch", &sketch_name(*sketch)), ("number", &index.saturating_add(1).to_string())])
solidcraft_ui_text::format("dialog.selection_external_point", &[("point", &point), ("sketch", &sketch_name(*sketch))])
}
Sel::SketchConstraint { id } => solidcraft_ui_text::format("dialog.selection_constraint", &[("id", id)]),
Sel::Profile { sketch, index } => solidcraft_ui_text::format(
"dialog.selection_profile",
&[("sketch", &sketch_name(*sketch)), ("number", &index.saturating_add(1).to_string())],
),
Sel::Plane { name } => names.label(name).to_string(),
Sel::Axis { name } | Sel::ConstructionPoint { name } | Sel::Body { name } => name.clone(),
Sel::Face { body, index, .. } if axis => format!("{body} · face {index} axis"),
@@ -3045,18 +3048,18 @@ pub(crate) fn input_rows_range(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, n
let filled = !inp.items.is_empty() || custom_axis || stored_reference.is_some();
let text = if filled {
RichText::new(if stored_reference.is_some() && matches!(d.kind, Kind::Section { .. }) {
crate::i18n::tr("ui.viewport_status_section_plane").into()
solidcraft_ui_text::tr("ui.viewport_status_section_plane").into()
} else if custom_axis || stored_reference.is_some() {
crate::i18n::tr("dialog.custom_axis").into()
solidcraft_ui_text::tr("dialog.custom_axis").into()
} else if !inp.multi {
inp.items.first().map(|item| selection_label(s, names, d, inp, item)).unwrap_or_default()
} else {
let count = if i == 0 { shell_count.unwrap_or(inp.items.len()) } else { inp.items.len() };
crate::i18n::format("dialog.selected_count", &[("count", &count.to_string())])
solidcraft_ui_text::format("dialog.selected_count", &[("count", &count.to_string())])
})
.color(t.on_accent)
} else {
RichText::new(crate::i18n::tr("dialog.select")).color(t.text)
RichText::new(solidcraft_ui_text::tr("dialog.select")).color(t.text)
};
let selected_text = text.text().strip_prefix("↖ ").unwrap_or_else(|| text.text());
let prompt = if !filled && inp.label == "Edges" { tr("Select edges") } else { selected_text };
@@ -3086,7 +3089,7 @@ pub(crate) fn input_rows_range(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, n
hint(inp).to_string()
}
} else if let Some(id) = inp.tip {
crate::i18n::tr(id).to_string()
solidcraft_ui_text::tr(id).to_string()
} else {
format!("{} (click to pick into this input)", hint(inp))
};
@@ -3111,7 +3114,7 @@ pub(crate) fn input_rows_range(d: &mut Dialog, ui: &mut egui::Ui, s: &Session, n
if inp.multi {
let open = d.listed == Some(i);
let at = rect.translate(vec2(-t.space_24, 0.0));
let label = crate::i18n::tr_label(if open { "Hide the picked items" } else { "Show the picked items" });
let label = solidcraft_ui_text::tr_label(if open { "Hide the picked items" } else { "Show the picked items" });
let arrow = crate::ui_kit::disclosure_button_at(ui, at, open, label);
crate::scenario::publish_handle(&format!("dialog:list_toggle:{}", inp.label), arrow.rect.center());
if arrow.clicked() {
@@ -3243,7 +3246,7 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
// Under the fold in a short window, a new note scrolls into view once, as a refusal does.
let note_shown_id = egui::Id::unique("sc_dialog_pick_refused_shown");
if let Some(id) = d.pick_refused.filter(|_| d.error.is_none() && app.preview.error.is_none()) {
let note = crate::ui_kit::text(ui, crate::i18n::tr(id), crate::ui_kit::Text::Hint("pick_refused"));
let note = crate::ui_kit::text(ui, solidcraft_ui_text::tr(id), crate::ui_kit::Text::Hint("pick_refused"));
crate::scenario::publish_rect("dialog:pick_refused", note.rect);
if ui.data(|m| m.get_temp::<&'static str>(note_shown_id)) != Some(id) {
ui.scroll_to_rect(note.rect, None);
@@ -3275,13 +3278,13 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
ui.horizontal(|ui| {
let ready = solidcraft_engine::find_command(crate::refused_part::REPAIR).map(|c| c.info(&app.session));
let enabled = ready.as_ref().is_some_and(|i| i.enabled);
let repair = crate::ui_kit::button(ui, crate::i18n::tr("refused.repair"), crate::ui_kit::Button::Enabled(enabled));
let repair = crate::ui_kit::button(ui, solidcraft_ui_text::tr("refused.repair"), crate::ui_kit::Button::Enabled(enabled));
crate::scenario::publish_handle("dialog:repair_part", repair.rect.center());
if repair.clicked() {
crate::refused_part::repair(app, &part.body);
}
if part.face.is_some() {
let show = crate::ui_kit::button(ui, crate::i18n::tr("refused.show_face"), crate::ui_kit::Button::Normal);
let show = crate::ui_kit::button(ui, solidcraft_ui_text::tr("refused.show_face"), crate::ui_kit::Button::Normal);
crate::scenario::publish_handle("dialog:show_face", show.rect.center());
if show.clicked() {
crate::refused_part::show_face(app, &part);
@@ -3371,7 +3374,7 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
if let Some((_, rest)) = error.split_once(solidcraft_engine::kernel::SETBACK_AT_LEAST)
&& let Some(least) = rest.split_once(" mm").map(|(n, _)| n.trim()).filter(|n| n.parse::<f64>().is_ok())
{
return Some(FilletRefusal::Plain(crate::i18n::format("ui.fillet_error_setback_short", &[("least", &format!("{least} mm"))])));
return Some(FilletRefusal::Plain(solidcraft_ui_text::format("ui.fillet_error_setback_short", &[("least", &format!("{least} mm"))])));
}
let too_large = error.contains("larger than the faces beside its edges allow");
let corner = error.contains("setback") || error.contains("G2 corner") || error.contains("corner patch");
@@ -3409,7 +3412,7 @@ fn reveal_refusal(on_ok: bool, typing: bool, shown: Option<&str>, error: &str) -
/// message is in the catalogs.
fn shown_error(error: &str) -> String {
let msg = error.strip_prefix("invalid parameters for `").and_then(|rest| rest.split_once("`: ")).map_or(error, |(_, msg)| msg);
crate::i18n::tr_diagnostic(crate::i18n::tr_label(msg))
crate::i18n::tr_diagnostic(solidcraft_ui_text::tr_label(msg))
}
/// Find the Boundary Fill cells again when the tool bodies changed (keeping the choices of
@@ -3776,7 +3779,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
return;
}
crate::sketch_text::sync_path(&mut d);
let _language = crate::i18n::scope(&app.preferences.language);
let _language = solidcraft_ui_text::scope(&app.preferences.language);
if let Kind::TextMove(k) = &d.kind
&& !crate::sketch_text_move::valid(app, k)
{
@@ -3844,7 +3847,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
let wide = matches!(&d.kind, Kind::Motion(k) if k.wide());
let raw_title = title_of(&d);
let heading = if d.editing.is_some() { crate::i18n::format("dialog.edit_title", &[("title", tr(raw_title))]) } else { tr(raw_title).to_string() };
let heading =
if d.editing.is_some() { solidcraft_ui_text::format("dialog.edit_title", &[("title", tr(raw_title))]) } else { tr(raw_title).to_string() };
let frame = crate::ui_kit::Panel::feature_frame(ctx, app.ui.layout, side);
// OK is offered once the inputs make a feature.
let has_ok = !matches!(d.kind, Kind::Sketch | Kind::Measure { .. });
@@ -3949,7 +3953,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
}
let text = app.applying.as_ref().map(|job| {
let seconds = ((crate::now_ms() - job.started) / 1000.0).floor().max(0.0);
crate::i18n::format("dialog.computing", &[("seconds", &format!("{seconds:.0}"))])
solidcraft_ui_text::format("dialog.computing", &[("seconds", &format!("{seconds:.0}"))])
});
crate::ui_kit::ComputeStatus { text, busy: app.applying.is_some() || app.preview.busy, applying: app.applying.is_some() }
};
@@ -5765,10 +5769,10 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
}
Kind::Material { index, editor } => {
if editor.editing() {
return Err(crate::i18n::tr("material.editor.save_or_discard").to_string());
return Err(solidcraft_ui_text::tr("material.editor.save_or_discard").to_string());
}
need(0, "bodies")?;
let name = s.doc.material_names().get(*index).cloned().ok_or_else(|| crate::i18n::tr("material.selection.choose").to_string())?;
let name = s.doc.material_names().get(*index).cloned().ok_or_else(|| solidcraft_ui_text::tr("material.selection.choose").to_string())?;
("material.assign", json!({"bodies": sels(d, 0).body_names(), "material": name}))
}
Kind::Section { offset, flip, hatch } => {
@@ -8223,7 +8227,7 @@ mod awaiting_selection_tests {
h.step(&serde_json::json!({"key":"Enter"})).unwrap();
h.frames(2);
assert_eq!(h.app.dialog.as_ref().and_then(|d| d.error.as_deref()), Some(english), "{command}");
let _japanese = crate::i18n::scope("ja");
let _japanese = solidcraft_ui_text::scope("ja");
let shown = super::shown_error(english);
assert!(shown != english && !shown.is_ascii(), "{command} shows {shown:?}");
}
+10 -10
View File
@@ -453,7 +453,7 @@ fn motion_dialog_hints_are_translated() {
}
let mut h = fixture();
h.app.preferences.language = "ja".into();
let _japanese = crate::i18n::scope("ja");
let _japanese = solidcraft_ui_text::scope("ja");
for (command, english) in [("motion.study", "add a key for a joint first"), ("motion.trace", "select a point on a component")] {
let mut d = Dialog::for_command(&h.app, command).unwrap();
let mut painted = Vec::new();
@@ -472,7 +472,7 @@ fn motion_dialog_hints_are_translated() {
out.shapes.iter().for_each(|shape| texts(&shape.shape, &mut painted));
}
assert!(!painted.iter().any(|p| p == english), "{command} paints {english:?} in Japanese");
assert!(painted.iter().any(|p| p == crate::i18n::tr_label(english)), "{command} paints {painted:?}");
assert!(painted.iter().any(|p| p == solidcraft_ui_text::tr_label(english)), "{command} paints {painted:?}");
}
}
@@ -746,7 +746,7 @@ fn hand_drawn_dialog_texts_are_translated() {
}
let mut h = fixture();
h.app.preferences.language = "ja".into();
let _japanese = crate::i18n::scope("ja");
let _japanese = solidcraft_ui_text::scope("ja");
for (command, english) in [
("joint.gear", "▶ Play"),
("motion.study", "Loop"),
@@ -790,7 +790,7 @@ fn revolve_layout_fixture_builds_a_valid_solid() {
#[test]
fn new_cards_settle_before_the_first_click_in_both_layouts() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
for command in ["solid.box", "solid.revolve", "solid.shell", "joint.create"] {
let mut h = fixture();
h.call("ui.set", json!({"layout":mode}));
@@ -1401,13 +1401,13 @@ fn extrude_builds_the_one_region_clicked_where_the_face_outline_cuts_a_profile()
#[test]
fn every_selection_field_label_is_translated() {
let h = fixture();
let _japanese = crate::i18n::scope("ja");
let _japanese = solidcraft_ui_text::scope("ja");
let mut missing = std::collections::BTreeSet::new();
for spec in solidcraft_engine::command_specs() {
let Some(d) = Dialog::for_command(&h.app, spec.id) else { continue };
for input in &d.inputs {
let label = input.label.trim();
if label.chars().any(|c| c.is_ascii_alphabetic()) && crate::i18n::tr_label(label) == label {
if label.chars().any(|c| c.is_ascii_alphabetic()) && solidcraft_ui_text::tr_label(label) == label {
missing.insert(format!("{}: {label}", spec.id));
}
}
@@ -1570,9 +1570,9 @@ fn pattern_says_why_a_clicked_sketch_is_not_taken() {
}
}
assert!(crate::dialogs::missing_pick(crate::dialogs::SKETCH_NOT_OBJECT), "shown as what to pick, with no Create Issue");
let _japanese = crate::i18n::scope("ja");
assert!(!crate::i18n::tr_label(crate::dialogs::SKETCH_NOT_OBJECT).is_ascii(), "the hint reads in the user's language");
assert!(!crate::i18n::tr("ui.pattern_object_type_tip").is_ascii(), "so does the Object Type tooltip");
let _japanese = solidcraft_ui_text::scope("ja");
assert!(!solidcraft_ui_text::tr_label(crate::dialogs::SKETCH_NOT_OBJECT).is_ascii(), "the hint reads in the user's language");
assert!(!solidcraft_ui_text::tr("ui.pattern_object_type_tip").is_ascii(), "so does the Object Type tooltip");
}
/// Features stored with bare numbers edit with their units written out, as the Plastic ones do
@@ -1682,7 +1682,7 @@ fn dialog_sketch_selection_labels_translate_and_keep_identifiers() {
(Sel::SketchConstraint { id: "k3".into() }, "Constraint k3", "Bedingung k3"),
] {
for (language, expected) in [("en", english), ("de", german)] {
let _language = crate::i18n::scope(language);
let _language = solidcraft_ui_text::scope(language);
assert_eq!(selection_label(&h.app.session, h.app.preferences.origin_plane_names, &d, &d.inputs[0], &sel), expected);
}
}
+5 -3
View File
@@ -385,7 +385,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
// Apply To: bodies or faces (the input follows).
if let Some(component) = &k.component {
ui_kit::row(ui, "Apply To", |ui| ui_kit::text(ui, &format!("Component: {component}"), ui_kit::Text::Cell));
if ui_kit::row(ui, "", |ui| ui_kit::button(ui, crate::i18n::tr_label("Select bodies or faces instead"), ui_kit::Button::Normal)).clicked() {
if ui_kit::row(ui, "", |ui| ui_kit::button(ui, solidcraft_ui_text::tr_label("Select bodies or faces instead"), ui_kit::Button::Normal))
.clicked()
{
k.component = None;
}
} else {
@@ -462,7 +464,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
});
}
if ui_kit::button(ui, "Remove override", ui_kit::Button::Enabled(has_target))
.backing_on_hover_text(crate::i18n::tr_label("Restore the inherited component or physical material appearance"))
.backing_on_hover_text(solidcraft_ui_text::tr_label("Restore the inherited component or physical material appearance"))
.clicked()
&& let Some(p) = clear_params(k, &picked)
{
@@ -523,7 +525,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
.filter(|(_, l)| matches_library_filter(l, (k.category != 0).then_some(want.as_str()), &query, k.only_available))
.collect();
if shown.is_empty() {
ui_kit::text(ui, crate::i18n::tr_label("No matching appearances"), ui_kit::Text::Note("appearances_empty"));
ui_kit::text(ui, solidcraft_ui_text::tr_label("No matching appearances"), ui_kit::Text::Note("appearances_empty"));
}
ui_kit::swatch_grid(ui, &shown, |ui, (i, l)| {
let picked_look = k.look.as_deref() == Some(l.name.as_str()) && k.pbr == l.pbr;
+52 -47
View File
@@ -73,7 +73,7 @@ pub(crate) fn kind_label(kind: JointKind) -> &'static str {
/// A joint as the joint lists name it: its name, and its type in the user's language.
pub(crate) fn joint_choice(name: &str, kind: JointKind) -> String {
format!("{name} ({})", crate::i18n::tr_label(kind_label(kind)))
format!("{name} ({})", solidcraft_ui_text::tr_label(kind_label(kind)))
}
/// What a joint origin input accepts: snaps on faces, edges and vertices, the world origin, and
@@ -819,7 +819,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
}
}
Asm::NewComponent { name, activate } => {
enter |= text_row(ui, "Name", name, crate::i18n::tr_label("Component1")).0;
enter |= text_row(ui, "Name", name, solidcraft_ui_text::tr_label("Component1")).0;
crate::ui_kit::options(ui, "component_options", &mut [("Activate", activate)]);
}
Asm::Joint(f) => {
@@ -832,8 +832,8 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
ui.vertical(|ui| {
crate::ui_kit::status_line(ui, &why, crate::ui_kit::StatusTone::Warning);
if !bodies.is_empty()
&& crate::ui_kit::button(ui, crate::i18n::tr("cmd.component_from_bodies"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr("joint.from_bodies_hint"))
&& crate::ui_kit::button(ui, solidcraft_ui_text::tr("cmd.component_from_bodies"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(solidcraft_ui_text::tr("joint.from_bodies_hint"))
.clicked()
{
// A refusal shows on the status line; the picks stay either way.
@@ -869,9 +869,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
let kind = f.joint_kind();
if kind != JointKind::Rigid {
form_row(ui, "Animate", |ui| {
let label = crate::i18n::tr(if f.animate { "ui.joint_stop" } else { "ui.joint_play" });
let label = solidcraft_ui_text::tr(if f.animate { "ui.joint_stop" } else { "ui.joint_play" });
if crate::ui_kit::button(ui, label, crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr_label("Play the joint's motion in the view"))
.backing_on_hover_text(solidcraft_ui_text::tr_label("Play the joint's motion in the view"))
.clicked()
{
f.animate = !f.animate;
@@ -915,7 +915,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
}
}
Asm::JointOrigin { name, .. } => {
enter |= text_row(ui, "Name", name, crate::i18n::tr_label("Joint Origin1")).0;
enter |= text_row(ui, "Name", name, solidcraft_ui_text::tr_label("Joint Origin1")).0;
}
Asm::RigidGroup => {}
Asm::Drive { joint, values, focus } => {
@@ -927,9 +927,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
if let Some((id, name)) = &rigid {
let mut edit = false;
ui.vertical(|ui| {
let why = crate::i18n::format("drive.rigid", &[("joint", name.as_str())]);
let why = solidcraft_ui_text::format("drive.rigid", &[("joint", name.as_str())]);
crate::ui_kit::status_line(ui, &why, crate::ui_kit::StatusTone::Warning);
let r = crate::ui_kit::button(ui, crate::i18n::tr_label("Edit Joint"), crate::ui_kit::Button::Normal);
let r = crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Edit Joint"), crate::ui_kit::Button::Normal);
crate::scenario::publish_handle("drive:edit_joint", r.rect.center());
edit = r.clicked();
});
@@ -941,7 +941,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
if list.is_empty() {
if rigid.is_none() {
ui.vertical(|ui| {
ui.label(RichText::new(crate::i18n::tr_label("no joint that moves yet")).color(t.text_dim));
ui.label(RichText::new(solidcraft_ui_text::tr_label("no joint that moves yet")).color(t.text_dim));
});
}
return false;
@@ -1085,13 +1085,15 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
ui.horizontal(|ui| {
let seconds = ((crate::now_ms() - started) / 1000.0).floor().max(0.0);
ui.add(egui::Spinner::new().size(t.font_small));
ui.label(RichText::new(crate::i18n::format("dialog.computing", &[("seconds", &format!("{seconds:.0}"))])).color(t.text_dim));
ui.label(
RichText::new(solidcraft_ui_text::format("dialog.computing", &[("seconds", &format!("{seconds:.0}"))])).color(t.text_dim),
);
});
});
}
form_row(ui, "", |ui| {
if check.is_some() {
if crate::ui_kit::button(ui, crate::i18n::tr("ui.cancel"), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr("ui.cancel"), crate::ui_kit::Button::Normal).clicked() {
// Its thread finishes the check; the reply is left unread.
*check = None;
}
@@ -1100,7 +1102,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
let stale = result.is_some() && *of != picked;
if crate::ui_kit::button(
ui,
crate::i18n::tr(if stale { "dialog.compute_again" } else { "dialog.compute" }),
solidcraft_ui_text::tr(if stale { "dialog.compute_again" } else { "dialog.compute" }),
crate::ui_kit::Button::Normal,
)
.clicked()
@@ -1132,7 +1134,8 @@ fn find_limits_row(app: &mut SolveApp, ui: &mut egui::Ui, f: &mut JointForm, id:
let t = Tokens::get();
if let Some(search) = &f.search {
// A poisoned lock: the search's thread panicked.
let reply = search.reply.lock().map(|mut r| r.take()).unwrap_or_else(|_| Some(Err(crate::i18n::tr("joint.find_limits_failed").to_string())));
let reply =
search.reply.lock().map(|mut r| r.take()).unwrap_or_else(|_| Some(Err(solidcraft_ui_text::tr("joint.find_limits_failed").to_string())));
match reply {
Some(reply) => {
// Found for a design changed since (by a script, the control channel, undo), or
@@ -1143,7 +1146,7 @@ fn find_limits_row(app: &mut SolveApp, ui: &mut egui::Ui, f: &mut JointForm, id:
f.searched = Some(searched);
fill_limits(&app.session.doc, f, reply);
} else {
f.found_note = Some(crate::i18n::tr("joint.find_limits_stale").to_string());
f.found_note = Some(solidcraft_ui_text::tr("joint.find_limits_stale").to_string());
}
// The rows above were drawn before this: once more, with what was found.
ui.ctx().request_repaint();
@@ -1159,20 +1162,20 @@ fn find_limits_row(app: &mut SolveApp, ui: &mut egui::Ui, f: &mut JointForm, id:
{
f.searched = None;
f.found.clear();
f.found_note = Some(crate::i18n::tr("joint.find_limits_stale").to_string());
f.found_note = Some(solidcraft_ui_text::tr("joint.find_limits_stale").to_string());
}
form_row(ui, "", |ui| {
if f.search.is_some() {
ui.horizontal(|ui| {
ui.add(egui::Spinner::new().size(t.font_small));
ui.label(RichText::new(crate::i18n::tr("joint.finding_limits")).color(t.text_dim));
if crate::ui_kit::button(ui, crate::i18n::tr("ui.cancel"), crate::ui_kit::Button::Normal).clicked() {
ui.label(RichText::new(solidcraft_ui_text::tr("joint.finding_limits")).color(t.text_dim));
if crate::ui_kit::button(ui, solidcraft_ui_text::tr("ui.cancel"), crate::ui_kit::Button::Normal).clicked() {
// Its thread finishes within its work budget; the reply is left unread.
f.search = None;
}
});
} else if crate::ui_kit::button(ui, crate::i18n::tr("joint.find_limits"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr("joint.find_limits_hint"))
} else if crate::ui_kit::button(ui, solidcraft_ui_text::tr("joint.find_limits"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(solidcraft_ui_text::tr("joint.find_limits_hint"))
.clicked()
{
find_limits(app, f, id, inputs);
@@ -1274,20 +1277,20 @@ fn fill_limits(doc: &solidcraft_engine::doc::Document, f: &mut JointForm, reply:
let (min, max) = (lo.map(shown), hi.map(shown));
let mut lines = Vec::new();
for (label, found, by) in [("Minimum", &min, &v["min_by"]), ("Maximum", &max, &v["max_by"])] {
let which = crate::i18n::tr_label(label);
let which = solidcraft_ui_text::tr_label(label);
let value = found.as_ref().map(|(_, shown)| shown.as_str()).unwrap_or_default();
lines.push(match by.as_str() {
Some("loop") => crate::i18n::format("joint.limit_loop", &[("side", which), ("value", value)]),
Some("limit") => crate::i18n::format("joint.limit_limit", &[("side", which), ("value", value)]),
Some("turn") => crate::i18n::format("joint.limit_turn", &[("side", which), ("value", value)]),
Some("loop") => solidcraft_ui_text::format("joint.limit_loop", &[("side", which), ("value", value)]),
Some("limit") => solidcraft_ui_text::format("joint.limit_limit", &[("side", which), ("value", value)]),
Some("turn") => solidcraft_ui_text::format("joint.limit_turn", &[("side", which), ("value", value)]),
Some(_) => {
// Down to a tenth, so it never claims more than was searched.
let range = (v["range"].as_f64().filter(|x| x.is_finite()).unwrap_or(0.0) / scale * 10.0).floor() / 10.0;
crate::i18n::format("joint.limit_range", &[("side", which), ("range", &num(range, &unit))])
solidcraft_ui_text::format("joint.limit_range", &[("side", which), ("range", &num(range, &unit))])
}
None => {
let (a, b) = (by["a"].as_str().unwrap_or_default(), by["b"].as_str().unwrap_or_default());
crate::i18n::format("joint.limit_contact", &[("side", which), ("value", value), ("a", a), ("b", b)])
solidcraft_ui_text::format("joint.limit_contact", &[("side", which), ("value", value), ("a", a), ("b", b)])
}
});
}
@@ -1308,7 +1311,7 @@ fn fill_limits(doc: &solidcraft_engine::doc::Document, f: &mut JointForm, reply:
.map(|o| format!("{} / {}", o["a"].as_str().unwrap_or_default(), o["b"].as_str().unwrap_or_default()))
.collect();
if !overlapping.is_empty() {
f.found_note = Some(crate::i18n::format("joint.limits_overlapping", &[("pairs", &overlapping.join(", "))]));
f.found_note = Some(solidcraft_ui_text::format("joint.limits_overlapping", &[("pairs", &overlapping.join(", "))]));
}
}
@@ -1343,7 +1346,7 @@ fn interference(app: &SolveApp, picked: &[Sel], clearance: &str) -> Check {
}
check_now(app.session.ahead(), &crate::open_meshing::session_world(&app.session), p)
}
_ => (json!({ "error": crate::i18n::tr("dialog.clearance_invalid") }), Vec::new()),
_ => (json!({ "error": solidcraft_ui_text::tr("dialog.clearance_invalid") }), Vec::new()),
};
if let Ok(mut slot) = check.reply.lock() {
*slot = Some(reply);
@@ -1389,7 +1392,7 @@ fn interference_rows(app: &SolveApp, ui: &mut egui::Ui, r: &Value) {
}
let hits = r["overlaps"].as_array().cloned().unwrap_or_default();
form_row(ui, "Checked", |ui| {
ui.label(crate::i18n::format("dialog.bodies_count", &[("count", &r["bodies"].as_u64().unwrap_or(0).to_string())]));
ui.label(solidcraft_ui_text::format("dialog.bodies_count", &[("count", &r["bodies"].as_u64().unwrap_or(0).to_string())]));
});
let unchecked = r["unchecked"].as_array().cloned().unwrap_or_default();
let near = r["near"].as_array().cloned().unwrap_or_default();
@@ -1400,7 +1403,7 @@ fn interference_rows(app: &SolveApp, ui: &mut egui::Ui, r: &Value) {
// Pairs closer than the clearance, nearest first.
if !near.is_empty() {
form_row(ui, "Too close", |ui| {
ui.label(RichText::new(crate::i18n::tr("ui.distance")).semibold().color(crate::theme::Tokens::get().text));
ui.label(RichText::new(solidcraft_ui_text::tr("ui.distance")).semibold().color(crate::theme::Tokens::get().text));
});
}
for h in &near {
@@ -1412,7 +1415,7 @@ fn interference_rows(app: &SolveApp, ui: &mut egui::Ui, r: &Value) {
let nearest = r["nearest"].as_object().cloned().unwrap_or_default();
if !nearest.is_empty() {
form_row(ui, "Nearest", |ui| {
ui.label(RichText::new(crate::i18n::tr("ui.distance")).semibold().color(crate::theme::Tokens::get().text));
ui.label(RichText::new(solidcraft_ui_text::tr("ui.distance")).semibold().color(crate::theme::Tokens::get().text));
});
}
for (body, list) in &nearest {
@@ -1426,7 +1429,7 @@ fn interference_rows(app: &SolveApp, ui: &mut egui::Ui, r: &Value) {
}
if !hits.is_empty() {
form_row(ui, "Interferes", |ui| {
ui.label(RichText::new(crate::i18n::tr_label("Volume")).semibold().color(crate::theme::Tokens::get().text));
ui.label(RichText::new(solidcraft_ui_text::tr_label("Volume")).semibold().color(crate::theme::Tokens::get().text));
});
}
for h in hits {
@@ -1437,7 +1440,7 @@ fn interference_rows(app: &SolveApp, ui: &mut egui::Ui, r: &Value) {
// Pairs the check could not work out: not clear, not known to interfere.
for u in unchecked {
form_row(ui, &format!("{} · {}", u["a"].as_str().unwrap_or(""), u["b"].as_str().unwrap_or("")), |ui| {
ui.label(RichText::new(crate::i18n::tr("dialog.could_not_be_checked")).color(t.warning))
ui.label(RichText::new(solidcraft_ui_text::tr("dialog.could_not_be_checked")).color(t.warning))
.backing_on_hover_text(u["reason"].as_str().unwrap_or(""));
});
}
@@ -1474,8 +1477,8 @@ fn joint_problem(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Option<(String
let occs: Vec<Option<u64>> = bodies.iter().map(|b| occurrence_of_body(&app.session, b)).collect();
if occs.first().is_some_and(|first| first.is_some() && occs.iter().all(|o| o == first)) {
return Some(match bodies.as_slice() {
[body] => (crate::i18n::format("joint.one_body", &[("body", body.as_str())]), Vec::new()),
_ => (crate::i18n::format("joint.one_component", &[("bodies", &bodies.join(", "))]), bodies),
[body] => (solidcraft_ui_text::format("joint.one_body", &[("body", body.as_str())]), Vec::new()),
_ => (solidcraft_ui_text::format("joint.one_component", &[("bodies", &bodies.join(", "))]), bodies),
});
}
commands(app, k, inputs).err().map(|e| (e, Vec::new()))
@@ -1509,7 +1512,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
Asm::FromBodies => {
let bodies = sel_items(inputs, 0).body_names();
if bodies.is_empty() {
return Err(crate::i18n::tr("ui.components_from_bodies_required").into());
return Err(solidcraft_ui_text::tr("ui.components_from_bodies_required").into());
}
cmd("component.from_bodies", json!({ "bodies": bodies }))
}
@@ -1599,12 +1602,12 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
let occs = occurrences(s, items);
if occs.len() < 2 {
if occs.len() == 1 && items.len() > 1 && items.iter().all(|x| matches!(x, Sel::Body { .. })) {
return Err(crate::i18n::format(
return Err(solidcraft_ui_text::format(
"joint.rigid_group_one_component",
&[("count", &items.len().to_string()), ("command", crate::i18n::tr("cmd.component_from_bodies"))],
&[("count", &items.len().to_string()), ("command", solidcraft_ui_text::tr("cmd.component_from_bodies"))],
));
}
return Err(crate::i18n::tr("joint.rigid_group_select_components").into());
return Err(solidcraft_ui_text::tr("joint.rigid_group_select_components").into());
}
cmd("joint.rigid_group", json!({ "occurrences": occs }))
}
@@ -1612,7 +1615,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
let j = s.doc.assembly.joints.get(*joint).ok_or("no joint that moves yet")?;
// A rigid joint has nothing to drive: the dialog says why and offers Edit Joint (#6981).
if j.kind == JointKind::Rigid {
return Err(crate::i18n::format("drive.rigid", &[("joint", &j.name)]));
return Err(solidcraft_ui_text::format("drive.rigid", &[("joint", &j.name)]));
}
cmd("joint.drive", json!({"joint": j.id, "values": values}))
}
@@ -1725,7 +1728,7 @@ pub fn world_preview(s: &Session, cmds: &[(String, Value)], colors: Colors) -> O
/// joined to nothing grounded moves nothing, and says which part that is.
pub fn drive_stop(reply: &Value) -> Option<String> {
if let Some(part) = reply.get("unconnected").and_then(Value::as_str) {
return Some(crate::i18n::format("drive.unconnected", &[("part", part)]));
return Some(solidcraft_ui_text::format("drive.unconnected", &[("part", part)]));
}
// The values come as joint.list gives them: degrees and millimetres.
let at: Vec<String> = reply
@@ -1739,10 +1742,10 @@ pub fn drive_stop(reply: &Value) -> Option<String> {
let at = at.join(", ");
let name = |v: &Value| v.as_str().unwrap_or_default().to_string();
if let Some(l) = reply.get("stopped_by_loop").filter(|v| v.is_object()) {
return Some(crate::i18n::format("drive.stopped_by_loop", &[("value", &at), ("joint", &name(&l["joint"]))]));
return Some(solidcraft_ui_text::format("drive.stopped_by_loop", &[("value", &at), ("joint", &name(&l["joint"]))]));
}
let c = reply.get("stopped_by_contact").filter(|v| v.is_object())?;
Some(crate::i18n::format("drive.stopped_by_contact", &[("value", &at), ("a", &name(&c["a"])), ("b", &name(&c["b"]))]))
Some(solidcraft_ui_text::format("drive.stopped_by_contact", &[("value", &at), ("a", &name(&c["a"])), ("b", &name(&c["b"]))]))
}
// ---- overlay ----
@@ -1848,20 +1851,22 @@ fn joint_menu(
open: &mut Option<Dialog>,
run: &mut Option<(&'static str, Value)>,
) {
if crate::ui_kit::button(ui, crate::i18n::tr_label("Edit Joint"), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Edit Joint"), crate::ui_kit::Button::Normal).clicked() {
*open = edit_joint(app, id);
ui.close();
}
if kind != JointKind::Rigid && crate::ui_kit::button(ui, crate::i18n::tr_label("Drive Joint"), crate::ui_kit::Button::Normal).clicked() {
if kind != JointKind::Rigid && crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Drive Joint"), crate::ui_kit::Button::Normal).clicked() {
*open = drive_joint(app, id);
ui.close();
}
if crate::ui_kit::button(ui, crate::i18n::tr_label(if suppressed { "Unsuppress" } else { "Suppress" }), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label(if suppressed { "Unsuppress" } else { "Suppress" }), crate::ui_kit::Button::Normal)
.clicked()
{
*run = Some(("joint.edit", json!({"joint": id, "suppressed": !suppressed})));
ui.close();
}
ui.separator();
if crate::ui_kit::button(ui, crate::i18n::tr_label("Delete"), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Delete"), crate::ui_kit::Button::Normal).clicked() {
*run = Some(("selection.delete", json!({ "joints": [id] })));
ui.close();
}
+11 -7
View File
@@ -740,9 +740,9 @@ fn picks_on_one_component_say_why_and_offer_components() {
frame(&mut app, vec![]);
}
let out = frame(&mut app, vec![]);
let why = crate::i18n::format("joint.one_component", &[("bodies", "Body1, Body2")]);
let why = solidcraft_ui_text::format("joint.one_component", &[("bodies", "Body1, Body2")]);
assert!(text_at(&out, &why).is_some(), "the dialog says why");
let at = text_at(&out, crate::i18n::tr("cmd.component_from_bodies")).expect("and offers the way out");
let at = text_at(&out, solidcraft_ui_text::tr("cmd.component_from_bodies")).expect("and offers the way out");
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, vec![egui::Event::PointerMoved(at)]);
frame(&mut app, vec![button(true)]);
@@ -1334,7 +1334,7 @@ fn components_from_bodies_dialog_select_all_cancel_and_ok() {
assert!(matches!(d.kind, Kind::Assembly(Asm::FromBodies)));
assert!(apply_commands(&h.app, d).is_err(), "OK requires at least one body");
assert_eq!(h.app.session.doc, before);
h.step(&json!({"widget":crate::i18n::tr("ui.select_all")})).unwrap();
h.step(&json!({"widget":solidcraft_ui_text::tr("ui.select_all")})).unwrap();
let commands = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
assert_eq!(commands, vec![("component.from_bodies".into(), json!({"bodies":names}))]);
if let Ok(path) = std::env::var("SOLIDCRAFT_COMPONENTS_DIALOG_EVIDENCE") {
@@ -1361,7 +1361,7 @@ fn components_from_bodies_dialog_select_all_cancel_and_ok() {
h.step(&json!({"start":"component.from_bodies"})).unwrap();
let d = h.app.dialog.as_ref().unwrap();
assert_eq!(d.inputs[0].items.body_names(), vec![names[0].clone()]);
h.step(&json!({"widget":crate::i18n::tr("ui.select_all")})).unwrap();
h.step(&json!({"widget":solidcraft_ui_text::tr("ui.select_all")})).unwrap();
h.step(&json!({"widget":"OK"})).unwrap();
assert!(h.app.dialog.is_none());
for name in &names {
@@ -1415,8 +1415,8 @@ fn find_limits_fills_the_limits_from_where_the_door_meets_the_block() {
frame(app, vec![button(true)]);
frame(app, vec![button(false)]);
};
click(&mut app, &crate::i18n::tr_label("Limits").to_uppercase());
click(&mut app, crate::i18n::tr("joint.find_limits"));
click(&mut app, &solidcraft_ui_text::tr_label("Limits").to_uppercase());
click(&mut app, solidcraft_ui_text::tr("joint.find_limits"));
assert_eq!(*app.session.doc, *before, "nothing changes until OK");
let Some(Kind::Assembly(Asm::Joint(f))) = app.dialog.as_ref().map(|d| &d.kind) else { panic!("the dialog stays open") };
// The door's face 4 mm off its hinge line meets the block's corner (80, 30) opening, and the
@@ -1455,7 +1455,11 @@ fn find_limits_searches_the_joint_as_edited_and_leaves_a_free_side() {
// from it the other, where nothing is in its way.
assert_eq!(f.limits[0].0, untouched.0, "one side found: the row stays as it was: {:?}", f.found);
assert!(f.limits[0].1 == untouched.1 || f.limits[0].2 == untouched.2, "{:?} {:?}", f.limits, f.found);
assert!(f.found[0].contains(crate::i18n::tr_label("Minimum")) && f.found[0].contains(crate::i18n::tr_label("Maximum")), "{:?}", f.found);
assert!(
f.found[0].contains(solidcraft_ui_text::tr_label("Minimum")) && f.found[0].contains(solidcraft_ui_text::tr_label("Maximum")),
"{:?}",
f.found
);
assert!(f.found_note.is_none(), "{:?}", f.found_note);
// Edited after the search, the joint is no longer the one searched (its notes then go).
let searched = f.searched.clone().expect("what was searched");
+5 -5
View File
@@ -116,25 +116,25 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, f: &mut CamForm, inputs: &[Se
joint_row(ui, "Cam", "cam_a", &cams, &mut f.a);
joint_row(ui, "Follower", "cam_b", &followers, &mut f.b);
form_row(ui, "Type", |ui| {
let labels: Vec<&str> = FOLLOWER_LABELS.iter().map(|l| crate::i18n::tr_label(l)).collect();
let labels: Vec<&str> = FOLLOWER_LABELS.iter().map(|l| solidcraft_ui_text::tr_label(l)).collect();
combo(ui, "cam_follower", &labels, &mut f.follower);
});
if FOLLOWERS.get(f.follower) == Some(&"roller") {
form_row(ui, "Radius", |ui| match (pick(inputs, ROLLER), picked_radius(s, inputs, ROLLER)) {
(_, Some(r)) => {
ui.label(format!("{} mm ({})", trim(r), crate::i18n::tr_label("picked")));
ui.label(format!("{} mm ({})", trim(r), solidcraft_ui_text::tr_label("picked")));
}
(Some(_), None) => {
ui.label(RichText::new(crate::i18n::tr_label("not a round face")).color(t.warning));
ui.label(RichText::new(solidcraft_ui_text::tr_label("not a round face")).color(t.warning));
}
(None, None) => enter |= field(ui, &mut f.radius),
});
}
crate::ui_kit::options(ui, "cam_options", &mut [("Other side", &mut f.flip)]);
form_row(ui, "Animate", |ui| {
let label = crate::i18n::tr(if f.animate { "ui.joint_stop" } else { "ui.joint_play" });
let label = solidcraft_ui_text::tr(if f.animate { "ui.joint_stop" } else { "ui.joint_play" });
let r = crate::ui_kit::button(ui, label, crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr_label("Play the joint's motion in the view"));
.backing_on_hover_text(solidcraft_ui_text::tr_label("Play the joint's motion in the view"));
crate::scenario::publish_handle("dialog:Animate", r.rect.center());
if r.clicked() {
f.animate = !f.animate;
+2 -2
View File
@@ -105,9 +105,9 @@ fn a_picked_cam_edge_makes_the_cam() {
/// The dialog's rows pass their English text, which the catalogs translate by its key.
#[test]
fn the_cam_dialog_labels_translate() {
let de = crate::i18n::Locale::for_language("de");
let de = solidcraft_ui_text::Locale::for_language("de");
for (english, german) in [("Cam", "Nocken"), ("Follower", "Stößel"), ("Roller", "Rolle"), ("Tip (optional)", "Spitze (optional)")] {
assert_eq!(de.label(english), german);
}
assert_eq!(crate::i18n::Locale::for_language("zh").label("Knife edge"), "尖顶");
assert_eq!(solidcraft_ui_text::Locale::for_language("zh").label("Knife edge"), "尖顶");
}
+4 -4
View File
@@ -160,11 +160,11 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, f: &mut CouplingForm, inputs:
form_row(ui, label, |ui| {
match (pick(inputs, i), picked_radius(s, inputs, i)) {
(_, Some(r)) => {
ui.label(format!("{} mm ({})", trim(r), crate::i18n::tr_label("picked")));
ui.label(format!("{} mm ({})", trim(r), solidcraft_ui_text::tr_label("picked")));
}
// A pick on a flat face or a straight edge has no radius: say so rather than fail on OK.
(Some(_), None) => {
ui.label(RichText::new(crate::i18n::tr_label("not a round face")).color(t.warning));
ui.label(RichText::new(solidcraft_ui_text::tr_label("not a round face")).color(t.warning));
}
(None, None) => *enter |= field(ui, text),
}
@@ -205,9 +205,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, f: &mut CouplingForm, inputs:
ui.label(RichText::new(shown.unwrap_or_else(|| "-".into())).color(t.text_dim));
});
form_row(ui, "Animate", |ui| {
let label = crate::i18n::tr(if f.animate { "ui.joint_stop" } else { "ui.joint_play" });
let label = solidcraft_ui_text::tr(if f.animate { "ui.joint_stop" } else { "ui.joint_play" });
let r = crate::ui_kit::button(ui, label, crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr_label("Play the joint's motion in the view"));
.backing_on_hover_text(solidcraft_ui_text::tr_label("Play the joint's motion in the view"));
crate::scenario::publish_handle("dialog:Animate", r.rect.center());
if r.clicked() {
f.animate = !f.animate;
+4 -4
View File
@@ -9,7 +9,7 @@ use crate::dialogs::{Kind, apply_commands, preview_commands};
#[test]
fn missing_joint_messages_are_drawn_in_full() {
for language in ["en", "de", "vi"] {
let _language = crate::i18n::scope(language);
let _language = solidcraft_ui_text::scope(language);
for (command, message) in [
("joint.screw", "needs a joint that turns and one that slides"),
("joint.gear", "needs two joints that turn"),
@@ -44,7 +44,7 @@ fn missing_joint_messages_are_drawn_in_full() {
.save(std::path::Path::new(&dir).join(format!("{command}-{language}.png")))
.unwrap();
}
let message = crate::i18n::tr_label(message);
let message = solidcraft_ui_text::tr_label(message);
let text = crate::test_support::text_shapes(&output).find(|text| text.galley.text() == message).expect("missing-joint message");
assert!(!text.galley.elided, "{command} ({language}) cuts off: {message}");
let bounds = text.galley.rect.translate(text.pos.to_vec2());
@@ -220,7 +220,7 @@ fn play_turns_the_angle_of_a_planar_pinion() {
/// The dialog's rows pass their English text, which the catalogs translate by its key.
#[test]
fn the_dialog_labels_translate() {
let de = crate::i18n::Locale::for_language("de");
let de = solidcraft_ui_text::Locale::for_language("de");
for (english, german) in [
("Gear 1", "Zahnrad 1"),
("Radius 2", "Radius 2"),
@@ -230,5 +230,5 @@ fn the_dialog_labels_translate() {
] {
assert_eq!(de.label(english), german);
}
assert_eq!(crate::i18n::Locale::for_language("ja").label("not a round face"), "円筒面ではありません");
assert_eq!(solidcraft_ui_text::Locale::for_language("ja").label("not a round face"), "円筒面ではありません");
}
+18 -17
View File
@@ -1,5 +1,6 @@
//! Dense ports of the common feature forms. Commands and parameter construction stay in dialogs.
use super::*;
use crate::dialogs_surface::continuity_row_explained;
use crate::ui_kit;
use crate::ui_kit::{choice_row, expr_row, hint_row};
use solidcraft_engine::kernel::Continuity;
@@ -127,7 +128,7 @@ fn setback_factor() -> String {
/// The Corners row's hover text: a line for each choice.
fn fillet_corner_tip() -> String {
let factor = setback_factor();
FILLET_CORNERS.map(|(_, tip)| crate::i18n::format(tip, &[("factor", &factor)])).join("\n")
FILLET_CORNERS.map(|(_, tip)| solidcraft_ui_text::format(tip, &[("factor", &factor)])).join("\n")
}
/// What `solid.fillet` does with a radius too large for the faces beside an edge (`fit`): stop
@@ -213,7 +214,7 @@ fn fillet_corner_rows(
}
let mut enter = false;
let mut corner = if patch { Continuity::G2 } else { Continuity::G1 };
ui_kit::continuity_row_explained(ui, "ui.corners", "fi_corner", &mut corner, &FILLET_CORNERS.map(|(g, _)| g), &fillet_corner_tip());
continuity_row_explained(ui, "ui.corners", "fi_corner", &mut corner, &FILLET_CORNERS.map(|(g, _)| g), &fillet_corner_tip());
if (corner == Continuity::G2) != patch {
if corner == Continuity::G2 {
extra.insert("corner".into(), json!(Continuity::G2.as_str()));
@@ -233,13 +234,13 @@ fn fillet_corner_rows(
let least = r.and_then(|r| crate::dialogs::fillet_least_setback(ui.ctx(), s, items, &corners, r));
let warning = least
.filter(|least| length(&shown).is_some_and(|v| v <= least * (1.0 + 1e-9)))
.map(|least| crate::i18n::format("ui.fillet_setback_least", &[("least", &crate::length_input::shown(&s.doc, least))]));
.map(|least| solidcraft_ui_text::format("ui.fillet_setback_least", &[("least", &crate::length_input::shown(&s.doc, least))]));
let mut text = shown.clone();
let row = ui.scope(|ui| {
let hint = crate::i18n::format("ui.fillet_setback_hint", &[("factor", &factor)]);
let hint = solidcraft_ui_text::format("ui.fillet_setback_hint", &[("factor", &factor)]);
ui_kit::checked_row(ui, "ui.fillet_setback", &mut text, &hint, warning.as_deref())
});
row.response.backing_on_hover_text(crate::i18n::format("ui.fillet_setback_tip", &[("factor", &factor)]));
row.response.backing_on_hover_text(solidcraft_ui_text::format("ui.fillet_setback_tip", &[("factor", &factor)]));
let (accepted, field) = row.inner;
enter |= accepted;
if text != shown {
@@ -358,7 +359,7 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
ui_kit::text(ui, &prompt, ui_kit::Text::Hint("tool")).backing_on_hover_text(&prompt);
if matches!(&d.kind, Kind::SketchModify(k) if k.previews()) {
let label = d.active_input().map(|input| tr(input.label)).unwrap_or_else(|| tr("Objects"));
let step = crate::i18n::format(
let step = solidcraft_ui_text::format(
"dialog.selection_step",
&[("step", &(d.active + 1).to_string()), ("total", &d.inputs.len().to_string()), ("input", label)],
);
@@ -937,7 +938,7 @@ fn additional_rows(app: &mut SolveApp, d: &mut Dialog, ui: &mut egui::Ui) -> boo
ui_kit::segmented(ui, &mut i, &["Emboss", "Deboss"], Default::default());
*deboss = i == 1;
});
ui.add(egui::Label::new(crate::i18n::tr("ui.emboss_wrap_note")).wrap());
ui.add(egui::Label::new(solidcraft_ui_text::tr("ui.emboss_wrap_note")).wrap());
}
Kind::Align { flip, components, axes: [ax, ay, az], angle } => {
// Where on the picked face or edge to snap: as picked (a face turns to meet
@@ -1138,7 +1139,7 @@ fn measure_rows(app: &SolveApp, ui: &mut egui::Ui, r: Option<&Value>, show: &mut
for (i, it) in r["items"].as_array().into_iter().flatten().enumerate() {
let lines = crate::quick_measure::item_lines(app, it);
for (k, line) in lines.into_iter().enumerate() {
let label = if k == 0 { crate::i18n::format("measure.item", &[("number", &(i + 1).to_string())]) } else { String::new() };
let label = if k == 0 { solidcraft_ui_text::format("measure.item", &[("number", &(i + 1).to_string())]) } else { String::new() };
// Coordinates need all three values; a fixed-height form cell elides the end.
let text = if label.is_empty() { line } else { format!("{label} {line}") };
ui_kit::text(ui, &text, ui_kit::Text::Report(&format!("measure_item_{i}_{k}"), false));
@@ -1158,7 +1159,7 @@ fn measure_rows(app: &SolveApp, ui: &mut egui::Ui, r: Option<&Value>, show: &mut
}
} else {
let text = crate::prefs::show_mm(app, d, 1);
form_row(ui, crate::i18n::tr_label("Distance"), |ui| {
form_row(ui, solidcraft_ui_text::tr_label("Distance"), |ui| {
let size = crate::theme::Tokens::get().form_close_size;
// The value keeps the room left of the button, so the button stays inside the row.
ui.scope(|ui| {
@@ -1166,7 +1167,7 @@ fn measure_rows(app: &SolveApp, ui: &mut egui::Ui, r: Option<&Value>, show: &mut
ui_kit::text(ui, &text, ui_kit::Text::Cell);
});
let (rect, _) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::hover());
let copy = ui_kit::icon_button_at(ui, rect, "copy", crate::i18n::tr_label("Copy"));
let copy = ui_kit::icon_button_at(ui, rect, "copy", solidcraft_ui_text::tr_label("Copy"));
crate::scenario::publish_handle("measure:copy:distance", copy.rect.center());
if copy.clicked() {
ui.ctx().copy_text(text.clone());
@@ -1181,7 +1182,7 @@ fn measure_rows(app: &SolveApp, ui: &mut egui::Ui, r: Option<&Value>, show: &mut
if let Some(a) = r["angle_deg"].as_f64()
&& r["items"].as_array().is_some_and(|v| v.len() == 2)
{
value(ui, crate::i18n::tr_label("Angle"), &format!("{a:.3}°"));
value(ui, solidcraft_ui_text::tr_label("Angle"), &format!("{a:.3}°"));
}
}
@@ -1202,7 +1203,7 @@ mod tests {
#[test]
fn common_forms_fit_both_layouts_and_scales_and_pin_footer_at_small_heights() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
for scale in [1.0, 2.0] {
for height in [950.0, 720.0] {
let mut h = Harness::new();
@@ -1244,7 +1245,7 @@ mod tests {
#[test]
fn tiny_windows_do_not_panic_or_publish_non_finite_cards() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
for size in [(1.0, 1.0), (40.0, 80.0), (180.0, 240.0)] {
let mut h = Harness::new();
h.call("ui.set", json!({"layout": mode}));
@@ -1551,7 +1552,7 @@ mod tests {
};
let shown = |h: &Harness| {
let tree = h.output.accesskit_update.as_ref().unwrap();
let label = crate::i18n::tr_label("ui.fillet_setback");
let label = solidcraft_ui_text::tr_label("ui.fillet_setback");
tree.nodes.iter().find(|(_, n)| n.label() == Some(label)).and_then(|(_, n)| n.value()).unwrap_or_default().to_string()
};
let typed = |h: &Harness| match &h.app.dialog.as_ref().unwrap().kind {
@@ -1612,10 +1613,10 @@ mod tests {
fn fillet_labels_use_words_not_continuity_codes() {
let coded = |text: &str| ["G0", "G1", "G2", "G3"].iter().any(|code| text.contains(code));
for label in FILLET_TYPES.map(|(label, _)| label).into_iter().chain(FILLET_CORNERS.map(|(g, _)| g.label_id())) {
assert!(!coded(crate::i18n::tr_label(label)), "{label}: {}", crate::i18n::tr_label(label));
assert!(!coded(solidcraft_ui_text::tr_label(label)), "{label}: {}", solidcraft_ui_text::tr_label(label));
}
for (g, tip) in FILLET_CORNERS {
assert!(crate::i18n::tr(tip).contains(g.code()), "the hover text keeps the code for those who know it: {tip}");
assert!(solidcraft_ui_text::tr(tip).contains(g.code()), "the hover text keeps the code for those who know it: {tip}");
}
let tip = fillet_corner_tip();
assert!(tip.contains("rolling") && tip.contains("1.5") && tip.contains("Setback"), "what each corner builds: {tip}");
@@ -1649,7 +1650,7 @@ mod tests {
// A blend too large for its faces keeps its own words.
assert_eq!(
crate::dialogs::fillet_plain_error("the fillet is larger than the faces beside its edges allow", true),
Some(crate::dialogs::FilletRefusal::Plain(crate::i18n::tr("ui.fillet_error_too_large").to_string()))
Some(crate::dialogs::FilletRefusal::Plain(solidcraft_ui_text::tr("ui.fillet_error_too_large").to_string()))
);
}
+1 -1
View File
@@ -70,7 +70,7 @@ pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
}
fn tr(text: &str) -> &str {
crate::i18n::tr_label(text)
solidcraft_ui_text::tr_label(text)
}
/// The dialog's own rows (after the selection inputs); true when Enter was pressed in a value.
+11 -11
View File
@@ -128,12 +128,12 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelInpu
let _ = inputs;
ui_kit::text(
ui,
crate::i18n::tr_label("Open local libraries in the desktop app. You can export this design's library here."),
solidcraft_ui_text::tr_label("Open local libraries in the desktop app. You can export this design's library here."),
ui_kit::Text::Note("library_platform"),
);
}
ui_kit::checkbox(ui, crate::i18n::tr_label("Replace existing library file"), &mut k.local.overwrite);
let save = ui_kit::button(ui, crate::i18n::tr_label("Export design library..."), ui_kit::Button::Normal);
ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Replace existing library file"), &mut k.local.overwrite);
let save = ui_kit::button(ui, solidcraft_ui_text::tr_label("Export design library..."), ui_kit::Button::Normal);
crate::scenario::publish_handle("library:export", save.rect.center());
if save.clicked() {
if app.services.pick_save.is_some() {
@@ -144,7 +144,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelInpu
}
ui_kit::text(
ui,
crate::i18n::tr_label("Apply your appearance first. Exports assigned appearances and materials created in this design."),
solidcraft_ui_text::tr_label("Apply your appearance first. Exports assigned appearances and materials created in this design."),
ui_kit::Text::Note("library_export"),
);
});
@@ -162,7 +162,7 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
k.local.selected = None;
}
ui.horizontal(|ui| {
if ui_kit::button(ui, crate::i18n::tr_label("Open library..."), ui_kit::Button::Normal).clicked() {
if ui_kit::button(ui, solidcraft_ui_text::tr_label("Open library..."), ui_kit::Button::Normal).clicked() {
open_library(app, k.local.token.clone());
}
let load_button = ui_kit::button(ui, "Load library", ui_kit::Button::Enabled(!k.local.path.is_empty()));
@@ -175,7 +175,7 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
});
if let Some(catalog) = &k.local.catalog {
ui_kit::text(ui, &catalog.name, ui_kit::Text::Report("library_name", false));
let mode = ui_kit::checkbox(ui, crate::i18n::tr_label("Physical material definitions"), &mut k.local.materials);
let mode = ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Physical material definitions"), &mut k.local.materials);
crate::scenario::publish_handle("library:materials", mode.rect.center());
if mode.changed() {
k.local.selected = None;
@@ -200,7 +200,7 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
.collect();
if appearances.is_empty() {
let message = if k.local.materials { "No matching physical materials." } else { "No matching appearance presets." };
ui_kit::text(ui, crate::i18n::tr_label(message), ui_kit::Text::Note("library_empty"));
ui_kit::text(ui, solidcraft_ui_text::tr_label(message), ui_kit::Text::Note("library_empty"));
}
let row_height = ui_kit::row_height();
ui_kit::list_frame(ui, "local_library_entries", |ui| {
@@ -267,7 +267,7 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
{
ui_kit::text(
ui,
&format!("{}: {} g/cm³", crate::i18n::tr("material.editor.assigned_density"), material.density_g_cm3),
&format!("{}: {} g/cm³", solidcraft_ui_text::tr("material.editor.assigned_density"), material.density_g_cm3),
ui_kit::Text::Report("library_material_density", false),
);
if let Some(card) = &material.physical_card {
@@ -276,12 +276,12 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
}
ui_kit::text(
ui,
crate::i18n::tr_label("Copies the definition only. Assign it from Physical Material when ready."),
solidcraft_ui_text::tr_label("Copies the definition only. Assign it from Physical Material when ready."),
ui_kit::Text::Note("library_material_copy"),
);
let copy = ui_kit::button(
ui,
crate::i18n::tr_label("Import material into design"),
solidcraft_ui_text::tr_label("Import material into design"),
ui_kit::Button::Enabled(entry.is_some() && !k.local.material_name.trim().is_empty()),
);
crate::scenario::publish_handle("library:material-import", copy.rect.center());
@@ -290,7 +290,7 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
{
k.note = Some(
match app.run("library.apply", json!({"path":k.local.path,"entry":entry.id,"revision":entry.revision,"name":k.local.material_name})) {
Ok(_) => crate::i18n::tr_label("Material copied. Existing body assignments are unchanged.").to_string(),
Ok(_) => solidcraft_ui_text::tr_label("Material copied. Existing body assignments are unchanged.").to_string(),
Err(error) => error,
},
);
+31 -25
View File
@@ -208,7 +208,7 @@ fn transport(ui: &mut egui::Ui, what: Transport, tip: &str) -> egui::Response {
p.add(bar(c.x + 6.0));
}
}
resp.backing_on_hover_text(crate::i18n::tr_label(tip))
resp.backing_on_hover_text(solidcraft_ui_text::tr_label(tip))
}
/// Study `key` as the Motion Study dialog edits it: (id, name, steps, keys as tracks with values
@@ -423,7 +423,7 @@ fn export_animation(app: &mut SolveApp, params: Value, name: &str) {
)
});
match made {
Ok(_) => app.set_status(crate::i18n::format("ui.saved_to", &[("path", &path)]), false),
Ok(_) => app.set_status(solidcraft_ui_text::format("ui.saved_to", &[("path", &path)]), false),
Err(e) => app.set_status(e.to_string(), true),
}
});
@@ -459,7 +459,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
let list = app.session.execute("contact.list", &json!({})).unwrap_or(Value::Null);
form_row(ui, "Contact", |ui| {
let mut on = list.get("enabled").and_then(Value::as_bool).unwrap_or(false);
if crate::ui_kit::checkbox(ui, crate::i18n::tr_label("Enabled"), &mut on).changed() {
if crate::ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Enabled"), &mut on).changed() {
let _ = app.run("contact.enable_sets", json!({ "enabled": on }));
}
});
@@ -469,14 +469,20 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
crate::ui_kit::section(ui, format!("contact:{n}"), &n, true, |ui| {
ui.horizontal(|ui| {
let count = s.get("occurrences").and_then(Value::as_array).map_or(0, Vec::len);
ui.label(RichText::new(crate::i18n::format("contact.components_count", &[("count", &count.to_string())])).color(t.text_dim));
if crate::ui_kit::button(ui, crate::i18n::tr_label(if off { "Unsuppress" } else { "Suppress" }), crate::ui_kit::Button::Small)
.clicked()
ui.label(
RichText::new(solidcraft_ui_text::format("contact.components_count", &[("count", &count.to_string())])).color(t.text_dim),
);
if crate::ui_kit::button(
ui,
solidcraft_ui_text::tr_label(if off { "Unsuppress" } else { "Suppress" }),
crate::ui_kit::Button::Small,
)
.clicked()
{
let _ = app.run("contact.edit", json!({"set": n, "suppressed": !off}));
}
if crate::ui_kit::button(ui, crate::i18n::tr_label("×"), crate::ui_kit::Button::Small)
.backing_on_hover_text(crate::i18n::tr_label("Delete the set"))
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("×"), crate::ui_kit::Button::Small)
.backing_on_hover_text(solidcraft_ui_text::tr_label("Delete the set"))
.clicked()
{
let _ = app.run("contact.edit", json!({"set": n, "delete": true}));
@@ -485,7 +491,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
});
}
ui.vertical(|ui| {
if crate::ui_kit::button(ui, crate::i18n::tr_label("Check collisions"), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Check collisions"), crate::ui_kit::Button::Normal).clicked() {
*check = Some(app.session.execute("contact.check", &json!({})).unwrap_or_else(|e| json!({"error": e.to_string()})));
}
});
@@ -493,9 +499,9 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
let hits = c.get("collisions").and_then(Value::as_array).cloned().unwrap_or_default();
crate::ui_kit::section(ui, "contact_collisions", "Collisions", true, |ui| {
if let Some(e) = c.get("error").and_then(Value::as_str) {
ui.label(RichText::new(crate::i18n::tr_label(e)).color(t.error));
ui.label(RichText::new(solidcraft_ui_text::tr_label(e)).color(t.error));
} else if hits.is_empty() {
ui.label(RichText::new(crate::i18n::tr_label("none")).color(t.text_dim));
ui.label(RichText::new(solidcraft_ui_text::tr_label("none")).color(t.text_dim));
} else {
ui.vertical(|ui| {
for h in hits {
@@ -533,7 +539,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel
}
if let Some(e) = error {
ui.vertical(|ui| {
ui.label(RichText::new(crate::i18n::tr_label(e)).color(t.error));
ui.label(RichText::new(solidcraft_ui_text::tr_label(e)).color(t.error));
});
}
}
@@ -569,7 +575,7 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
let moving = joints_moving(app);
if moving.is_empty() {
ui.vertical(|ui| {
ui.label(RichText::new(crate::i18n::tr_label("no joint that moves yet")).color(t.text_dim));
ui.label(RichText::new(solidcraft_ui_text::tr_label("no joint that moves yet")).color(t.text_dim));
});
return enter;
}
@@ -602,7 +608,7 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
*playing = false;
}
ui.add_space(t.space_6);
crate::ui_kit::checkbox(ui, crate::i18n::tr_label("Loop"), looping);
crate::ui_kit::checkbox(ui, solidcraft_ui_text::tr_label("Loop"), looping);
let names: Vec<String> = SPEEDS.iter().map(|x| format!("{x}×")).collect();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
ui.allocate_ui(vec2(t.form_label_width, t.control_height), |ui| combo(ui, "ms_speed", &refs, speed));
@@ -644,12 +650,12 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
let labels: Vec<String> = moving.iter().map(|(_, n, _)| n.clone()).collect();
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
let mut pos = moving.iter().position(|(i, _, _)| *i == tr.joint).unwrap_or(0);
form_row(ui, &crate::i18n::format("motion.track_name", &[("number", &(ti + 1).to_string())]), |ui| {
form_row(ui, &solidcraft_ui_text::format("motion.track_name", &[("number", &(ti + 1).to_string())]), |ui| {
ui.horizontal(|ui| {
let width = (ui.available_width() - t.control_height - t.space_8).max(0.0);
ui.allocate_ui(vec2(width, t.control_height), |ui| combo(ui, &format!("ms_joint{ti}"), &refs, &mut pos));
if crate::ui_kit::button(ui, crate::i18n::tr_label("×"), crate::ui_kit::Button::Small)
.backing_on_hover_text(crate::i18n::tr_label("Remove the track"))
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("×"), crate::ui_kit::Button::Small)
.backing_on_hover_text(solidcraft_ui_text::tr_label("Remove the track"))
.clicked()
{
remove = Some(ti);
@@ -678,8 +684,8 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
if crate::ui_kit::button(ui, crate::i18n::tr_label("Key here"), crate::ui_kit::Button::Small)
.backing_on_hover_text(crate::i18n::tr_label("Add a key at this step with the joint's value now"))
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Key here"), crate::ui_kit::Button::Small)
.backing_on_hover_text(solidcraft_ui_text::tr_label("Add a key at this step with the joint's value now"))
.clicked()
&& !tr.points.iter().any(|(s, _)| *s == now)
{
@@ -694,7 +700,7 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
tr.points.push((now, format!("0 {unit}")));
tr.points.sort_by_key(|p| p.0);
}
if crate::ui_kit::button(ui, crate::i18n::tr_label("Delete key"), crate::ui_kit::Button::Small).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Delete key"), crate::ui_kit::Button::Small).clicked() {
tr.points.retain(|(s, _)| *s != now);
}
});
@@ -706,16 +712,16 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
let mut export = false;
ui.vertical(|ui| {
ui.horizontal(|ui| {
if crate::ui_kit::button(ui, crate::i18n::tr_label("Add Key"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr_label("Key a joint's value at this step"))
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Add Key"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(solidcraft_ui_text::tr_label("Key a joint's value at this step"))
.clicked()
&& let Some((ji, _, kind)) = moving.first()
{
let unit = if kind.is_angle(0) { "deg" } else { crate::length_input::unit(&app.session.doc) };
tracks.push(Track { joint: *ji, index: 0, points: vec![(now, format!("0 {unit}"))] });
}
export = crate::ui_kit::button(ui, crate::i18n::tr_label("Export Animation…"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr_label("Save the study as an animated GIF, seen as the viewport shows it"))
export = crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Export Animation…"), crate::ui_kit::Button::Normal)
.backing_on_hover_text(solidcraft_ui_text::tr_label("Save the study as an animated GIF, seen as the viewport shows it"))
.clicked();
});
});
@@ -745,7 +751,7 @@ fn motion_study_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs:
}
if let Some(e) = error {
ui.vertical(|ui| {
ui.label(RichText::new(crate::i18n::tr_label(e)).color(t.text_dim));
ui.label(RichText::new(solidcraft_ui_text::tr_label(e)).color(t.text_dim));
});
}
enter
+6 -5
View File
@@ -285,7 +285,7 @@ fn collision_rows(
ui.vertical(|ui| {
ui.vertical(|ui| {
if hits.is_empty() {
ui.label(RichText::new(crate::i18n::tr("motion.no_collisions")).color(t.text_dim));
ui.label(RichText::new(solidcraft_ui_text::tr("motion.no_collisions")).color(t.text_dim));
}
for (i, c) in hits.iter().enumerate() {
let steps: Vec<String> = c["steps"]
@@ -298,7 +298,8 @@ fn collision_rows(
})
.collect();
let name = |k: &str| c[k].as_str().unwrap_or_default().to_string();
let text = crate::i18n::format("motion.collision", &[("a", &name("a")), ("b", &name("b")), ("steps", &steps.join(", "))]);
let text =
solidcraft_ui_text::format("motion.collision", &[("a", &name("a")), ("b", &name("b")), ("steps", &steps.join(", "))]);
// A click shows where they first collide.
let link = ui.link(RichText::new(text).color(t.error));
crate::scenario::publish_handle(&format!("motion:collision:{i}"), link.rect.center());
@@ -310,14 +311,14 @@ fn collision_rows(
}
if v["truncated"] == true {
let to = v["checked_to"].as_f64().map(|x| x.round().to_string()).unwrap_or_default();
ui.label(RichText::new(crate::i18n::format("motion.checked_to", &[("step", &to)])).color(t.warning));
ui.label(RichText::new(solidcraft_ui_text::format("motion.checked_to", &[("step", &to)])).color(t.warning));
}
});
});
}
Some(Err(e)) => {
ui.vertical(|ui| {
ui.add(egui::Label::new(RichText::new(crate::i18n::tr_label(e)).color(t.error)).wrap());
ui.add(egui::Label::new(RichText::new(solidcraft_ui_text::tr_label(e)).color(t.error)).wrap());
});
}
None => {}
@@ -385,7 +386,7 @@ fn plot_rows(
});
}
Some(Err(e)) => {
ui.add(egui::Label::new(RichText::new(crate::i18n::tr_label(e)).color(t.text_dim)).wrap());
ui.add(egui::Label::new(RichText::new(solidcraft_ui_text::tr_label(e)).color(t.text_dim)).wrap());
}
None => {}
});
+1 -1
View File
@@ -197,7 +197,7 @@ pub fn rows(app: &SolveApp, ui: &mut egui::Ui, k: &mut Resize) -> bool {
ui.label(configuration_name(&k.family.id, size, k.length));
});
}
crate::ui_kit::text(ui, crate::i18n::tr("ui.part_size_shared"), crate::ui_kit::Text::Hint("part_size_shared"));
crate::ui_kit::text(ui, solidcraft_ui_text::tr("ui.part_size_shared"), crate::ui_kit::Text::Hint("part_size_shared"));
false
}
+11 -7
View File
@@ -58,7 +58,7 @@ pub fn families(v: &Value) -> Vec<Family> {
/// name for a family the catalogs don't know.
pub(crate) fn family_name(f: &Family) -> String {
let id = format!("parts.family.{}", f.id);
match crate::i18n::tr(&id) {
match solidcraft_ui_text::tr(&id) {
shown if shown == id => f.name.clone(),
shown => shown.to_string(),
}
@@ -170,7 +170,11 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
};
if lib.is_empty() {
ui.vertical(|ui| {
crate::ui_kit::text(ui, crate::i18n::tr_label("the parts library is not available"), crate::ui_kit::Text::Note("parts_unavailable"));
crate::ui_kit::text(
ui,
solidcraft_ui_text::tr_label("the parts library is not available"),
crate::ui_kit::Text::Note("parts_unavailable"),
);
});
return false;
}
@@ -222,7 +226,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
k.length = Some(i);
});
}
text_row(ui, "Name", &mut k.name, crate::i18n::tr("parts.automatic_name"));
text_row(ui, "Name", &mut k.name, solidcraft_ui_text::tr("parts.automatic_name"));
// The part, and its table values.
let key = (k.family, k.size, k.length);
let tex_id = egui::Id::unique("sc_part_picture");
@@ -244,7 +248,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
ui.add(egui::Image::new(&tx).fit_to_exact_size(vec2(180.0, 112.0)).corner_radius(t.radius_sm));
}
None => {
crate::ui_kit::text(ui, crate::i18n::tr_label("no picture"), crate::ui_kit::Text::Note("parts_picture"));
crate::ui_kit::text(ui, solidcraft_ui_text::tr_label("no picture"), crate::ui_kit::Text::Note("parts_picture"));
}
});
if let Some((_, row)) = f.sizes.get(k.size)
@@ -264,7 +268,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Pt, inputs: &[SelInpu
}
if inputs.first().is_some_and(|i| i.items.is_empty()) {
ui.vertical(|ui| {
crate::ui_kit::text(ui, crate::i18n::tr("parts.placement_hint"), crate::ui_kit::Text::Note("parts_placement"));
crate::ui_kit::text(ui, solidcraft_ui_text::tr("parts.placement_hint"), crate::ui_kit::Text::Note("parts_placement"));
});
}
false
@@ -332,7 +336,7 @@ mod tests {
fn family_names_follow_the_language() {
let l = families(&solidcraft_engine::Session::default().execute("parts.library", &json!({})).unwrap());
assert!(l.len() >= 7, "{}", l.len());
let _german = crate::i18n::scope("de");
let _german = solidcraft_ui_text::scope("de");
for f in &l {
assert_ne!(family_name(f), f.name, "{}", f.id);
}
@@ -415,7 +419,7 @@ mod tests {
for f in &l {
for (size, row) in &f.sizes {
for key in row.as_object().into_iter().flat_map(|o| o.keys()).filter(|k| !matches!(k.as_str(), "size" | "default_length")) {
let label = dimension_label(key).map(crate::i18n::tr);
let label = dimension_label(key).map(solidcraft_ui_text::tr);
assert!(
label.is_some_and(|l| l.starts_with(char::is_uppercase) && !l.contains('_') && !l.starts_with("parts.")),
"{} {size}: {key} is labelled {label:?}",
+1 -1
View File
@@ -137,7 +137,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
/// A value row whose empty value means "from the rule" (or another default).
fn opt_field(ui: &mut egui::Ui, label: &str, v: &mut String, unit: &str, hint: &str) -> bool {
let hint = format!("{unit}, {}", crate::i18n::tr_label(hint));
let hint = format!("{unit}, {}", solidcraft_ui_text::tr_label(hint));
hint_row(ui, label, v, &hint)
}
+1 -1
View File
@@ -198,7 +198,7 @@ fn inch_plastic_rule_edit_preserves_material_angles_and_stored_lengths() {
/// face", …) are in the user's language: each has a message.
#[test]
fn optional_field_hints_have_messages() {
let ja = crate::i18n::Locale::for_language("ja");
let ja = solidcraft_ui_text::Locale::for_language("ja");
for hint in ["no hole", "to the face", "from the rule", "none", "at the face", "solid"] {
assert_ne!(ja.label(hint), hint, "`{hint}` has a Japanese message");
}
+25 -23
View File
@@ -178,10 +178,10 @@ fn spawn_wait(
/// Run `file.send_to_slicer` and report what happened. A slicer that has to be started first is
/// started and waited for in the background.
fn send_to_slicer(app: &mut SolveApp, params: Value) {
let _language = crate::i18n::scope(&app.preferences.language);
let _language = solidcraft_ui_text::scope(&app.preferences.language);
if let Some(wait) = &app.slicer_wait {
// Whatever was picked this time (another slicer, other bodies): nothing else is sent.
let line = crate::i18n::format("ui.slicer_still_starting", &[("slicer", &wait.slicer.name)]);
let line = solidcraft_ui_text::format("ui.slicer_still_starting", &[("slicer", &wait.slicer.name)]);
app.set_status(line, false);
return;
}
@@ -202,7 +202,7 @@ fn begin_wait(app: &mut SolveApp, params: Value, result: &Value) {
let spec = params["slicer"].as_str().unwrap_or_default().to_string();
let Some(slicer) = slicers::resolve(&spec) else {
// Started, but it can't be found again to be sent the file: say so rather than go quiet.
app.set_status(crate::i18n::format("ui.slicer_lost", &[("slicer", &name), ("path", &path)]), true);
app.set_status(solidcraft_ui_text::format("ui.slicer_lost", &[("slicer", &name), ("path", &path)]), true);
return;
};
// Same reading as `slicers::wait_for_ready`: when `busctl` is missing or fails, send; when it is
@@ -211,7 +211,7 @@ fn begin_wait(app: &mut SolveApp, params: Value, result: &Value) {
let done =
spawn_wait(move |left| probe.is_ready_within(left.min(slicers::PROBE_LIMIT)) != Some(false), slicers::READY_LIMIT, slicers::READY_POLL);
app.slicer_wait = Some(SlicerWait { done, slicer, result: result.clone() });
app.set_status(crate::i18n::format("ui.slicer_starting", &[("slicer", &name)]), false);
app.set_status(solidcraft_ui_text::format("ui.slicer_starting", &[("slicer", &name)]), false);
}
/// Per frame: when the slicer being waited for is ready, send the file; when the wait ran out, say so.
@@ -224,11 +224,11 @@ pub fn tick_wait(app: &mut SolveApp, ctx: &egui::Context) {
Err(TryRecvError::Disconnected) => false,
};
let Some(wait) = app.slicer_wait.take() else { return };
let _language = crate::i18n::scope(&app.preferences.language);
let _language = solidcraft_ui_text::scope(&app.preferences.language);
let path = wait.result.get("path").and_then(Value::as_str).unwrap_or_default().to_string();
if ready && path.is_empty() {
// Nothing was written to send (the answer had no path): say so, rather than open "".
app.set_status(crate::i18n::format("ui.slicer_no_file", &[("slicer", &wait.slicer.name)]), true);
app.set_status(solidcraft_ui_text::format("ui.slicer_no_file", &[("slicer", &wait.slicer.name)]), true);
} else if ready {
// The file is already written: open that one, rather than export the design again.
let mut result = wait.result;
@@ -243,7 +243,7 @@ pub fn tick_wait(app: &mut SolveApp, ctx: &egui::Context) {
}
report(app, &result);
} else {
app.set_status(crate::i18n::format("ui.slicer_no_response", &[("slicer", &wait.slicer.name), ("path", &path)]), true);
app.set_status(solidcraft_ui_text::format("ui.slicer_no_response", &[("slicer", &wait.slicer.name), ("path", &path)]), true);
}
}
@@ -338,7 +338,7 @@ fn sync_print_face(app: &mut SolveApp, k: &mut SaveMesh, inputs: &mut [SelInput]
for face in picked.iter().filter(|f| !shown.contains(f)) {
let Sel::Face { body, .. } = face else { continue };
if !bodies.contains(body) {
app.set_status(crate::i18n::tr("ui.print_face_body_not_sent"), true);
app.set_status(solidcraft_ui_text::tr("ui.print_face_body_not_sent"), true);
} else if !done.contains(body) {
crate::print_face_ui::set(app, body, Some(face));
done.push(body.clone());
@@ -371,7 +371,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut SaveMesh, inputs: &mu
// One row each: the two side by side do not fit the dialog's width.
for (label, visible) in [("Add all bodies", false), ("Add visible bodies", true)] {
form_row(ui, "", |ui| {
if crate::ui_kit::button(ui, crate::i18n::tr_label(label), crate::ui_kit::Button::Small).clicked()
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label(label), crate::ui_kit::Button::Small).clicked()
&& let Some(input) = inputs.first_mut()
{
input.items = body_picks(app, visible);
@@ -420,11 +420,11 @@ fn slicer_combo(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut SaveMesh) {
let current = entries.iter().find(|e| e.key == k.slicer);
let pad = "\u{2003}\u{2003}";
let easyprint = crate::easyprint_ui::available(app);
let easyprint_label = crate::i18n::tr("ui.easyprint_row");
let easyprint_label = solidcraft_ui_text::tr("ui.easyprint_row");
let text = match current {
Some(e) => format!("{pad}{}", e.slicer.name),
None if easyprint && k.slicer == EASYPRINT => format!("{pad}{easyprint_label}"),
None => crate::i18n::tr_label("No slicer found").to_string(),
None => solidcraft_ui_text::tr_label("No slicer found").to_string(),
};
let mut pick_custom = false;
let mut delete: Option<String> = None;
@@ -450,7 +450,7 @@ fn slicer_combo(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut SaveMesh) {
ui.painter().galley(at, text, Tokens::get().text);
crate::scenario::publish_handle(&format!("dialog:Slicer:{}", e.slicer.name), rect.center());
if e.user {
let hint = crate::i18n::format("ui.slicer_delete_tip", &[("program", e.slicer.command.first().map_or("", String::as_str))]);
let hint = solidcraft_ui_text::format("ui.slicer_delete_tip", &[("program", e.slicer.command.first().map_or("", String::as_str))]);
resp.clone().backing_on_hover_text(if elided { format!("{}\n{hint}", e.slicer.name) } else { hint });
if resp.hovered() && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Delete)) {
delete = Some(e.key.clone());
@@ -491,12 +491,12 @@ fn slicer_combo(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut SaveMesh) {
ui.painter().text(
rect.left_center() + vec2(ICON + 9.0, 0.0),
egui::Align2::LEFT_CENTER,
crate::i18n::tr_label("Custom…"),
solidcraft_ui_text::tr_label("Custom…"),
egui::FontId::proportional(crate::theme::Tokens::get().font_button),
Tokens::get().text,
);
crate::scenario::publish_handle("dialog:Slicer:Custom", rect.center());
if resp.backing_on_hover_text(crate::i18n::tr("ui.custom_program_tip")).clicked() {
if resp.backing_on_hover_text(solidcraft_ui_text::tr("ui.custom_program_tip")).clicked() {
pick_custom = true;
ui.close();
}
@@ -530,7 +530,7 @@ fn slicer_combo(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut SaveMesh) {
/// Put a program the user picked at the top of the list (and keep it).
fn add_custom(app: &mut SolveApp, path: &str) -> Result<(), String> {
if slicers::custom(path).is_none() {
return Err(crate::i18n::format("ui.not_a_program", &[("path", path)]));
return Err(solidcraft_ui_text::format("ui.not_a_program", &[("path", path)]));
}
let list = &mut app.preferences.custom_slicers;
list.retain(|p| p != path);
@@ -551,12 +551,12 @@ pub fn commands(k: &SaveMesh, inputs: &[SelInput]) -> Result<Vec<(String, Value)
}
let names = crate::selection::items(inputs, 0).body_names();
if names.is_empty() {
return Err(crate::i18n::tr("ui.save_mesh_pick_bodies").into());
return Err(solidcraft_ui_text::tr("ui.save_mesh_pick_bodies").into());
}
p["bodies"] = json!(names);
if k.open {
if k.slicer.is_empty() {
return Err(crate::i18n::tr("ui.save_mesh_no_slicer_open").into());
return Err(solidcraft_ui_text::tr("ui.save_mesh_no_slicer_open").into());
}
p["format"] = json!("3mf");
if k.slicer == EASYPRINT {
@@ -612,7 +612,7 @@ pub fn run(app: &mut SolveApp, k: &SaveMesh, inputs: &[SelInput]) -> Result<(),
/// Run `file.save_mesh` into `path` and report what happened.
fn save(app: &mut SolveApp, mut params: Value, path: &str) {
// Runs from the file picker's answer, outside the frame's language scope.
let _language = crate::i18n::scope(&app.preferences.language);
let _language = solidcraft_ui_text::scope(&app.preferences.language);
params["path"] = json!(path);
let Ok(result) = app.run("file.save_mesh", params) else { return };
report(app, &result);
@@ -626,19 +626,21 @@ fn report(app: &mut SolveApp, result: &Value) {
None => match result.get("slicer").and_then(Value::as_str) {
Some(s) => {
let mut line = match (result.get("numbered").and_then(Value::as_bool), std::path::Path::new(path).file_name()) {
(Some(true), Some(file)) => crate::i18n::format("ui.sent_to_slicer_copy", &[("slicer", s), ("file", &file.to_string_lossy())]),
_ => crate::i18n::format("ui.sent_to_slicer", &[("slicer", s)]),
(Some(true), Some(file)) => {
solidcraft_ui_text::format("ui.sent_to_slicer_copy", &[("slicer", s), ("file", &file.to_string_lossy())])
}
_ => solidcraft_ui_text::format("ui.sent_to_slicer", &[("slicer", s)]),
};
// Said once: sending the same bodies again overwrites the file the slicer has open.
if result.get("numbered").is_some() && !std::mem::replace(&mut app.preferences.slicer_resend_hint_seen, true) {
line = format!("{line}. {}", crate::i18n::tr("ui.slicer_resend_hint"));
line = format!("{line}. {}", solidcraft_ui_text::tr("ui.slicer_resend_hint"));
}
if result.get("stl_fallback").and_then(Value::as_bool) == Some(true) {
line = format!("{line}. {}", crate::i18n::tr("ui.slicer_stl_fallback"));
line = format!("{line}. {}", solidcraft_ui_text::tr("ui.slicer_stl_fallback"));
}
app.set_status(line, false);
}
None => app.set_status(crate::i18n::format("ui.saved_to", &[("path", path)]), false),
None => app.set_status(solidcraft_ui_text::format("ui.saved_to", &[("path", path)]), false),
},
}
}
+16 -16
View File
@@ -251,7 +251,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
Sm::Flange { ty, height, angle, radius, position, height_from, flip, distance, reverse, rule, own, relief, relief_size, then } => {
*ty = flange_type(inputs);
form_row(ui, "Type", |ui| {
ui.label(FLANGE_TYPES.get(*ty).copied().map(crate::i18n::tr).unwrap_or(""));
ui.label(FLANGE_TYPES.get(*ty).copied().map(solidcraft_ui_text::tr).unwrap_or(""));
});
match *ty {
1 => {
@@ -262,7 +262,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
// Further bends: each a flange on the far edge of the one before it.
form_row(ui, "Further Bends", |ui| {
let add = crate::ui_kit::button(ui, "Add bend", crate::ui_kit::Button::Normal)
.backing_on_hover_text(crate::i18n::tr_label("Another bend on the far edge of the last flange"));
.backing_on_hover_text(solidcraft_ui_text::tr_label("Another bend on the far edge of the last flange"));
if add.clicked() && then.len() < 16 {
then.push(("45 deg".into(), "20 mm".into(), false));
}
@@ -272,7 +272,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
let bend = n + 2;
// The bend's heading: what it makes, and a bin to take it out.
form_row(ui, &format!("Bend {bend}"), |ui| {
if crate::ui_kit::bin_button(ui, crate::i18n::tr_label("Remove this bend")).clicked() {
if crate::ui_kit::bin_button(ui, solidcraft_ui_text::tr_label("Remove this bend")).clicked() {
drop = Some(n);
}
ui.label(format!("{} · {}", a.trim(), h.trim()));
@@ -287,16 +287,16 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
then.remove(n);
}
crate::ui_kit::section(ui, "flange_advanced", "Advanced", false, |ui| {
enter |= hint_row(ui, "Bend Radius", radius, crate::i18n::tr_label("from the rule"));
enter |= hint_row(ui, "Bend Radius", radius, solidcraft_ui_text::tr_label("from the rule"));
// The relief at the corners these flanges make: the rule's unless set here.
choice_row(ui, "Corner Relief Shape", "sm_flange_relief", relief, &FLANGE_RELIEF_LABELS);
enter |= hint_row(ui, "Corner Relief", relief_size, crate::i18n::tr_label("from the rule"));
enter |= hint_row(ui, "Corner Relief", relief_size, solidcraft_ui_text::tr_label("from the rule"));
// With several edges, a row for each: an angle and a height of its own, left
// empty for the values above (shown as the hints).
let points = edge_point_list(inputs);
if points.len() > 1 || own.iter().any(|o| !(o.1.trim().is_empty() && o.2.trim().is_empty())) {
form_row(ui, "Per Edge", |ui| {
ui.label(RichText::new(crate::i18n::tr_label("angle, height")).weak())
ui.label(RichText::new(solidcraft_ui_text::tr_label("angle, height")).weak())
.backing_on_hover_text("An edge's own angle and height. Empty: the values above.");
});
for (n, p) in points.iter().enumerate() {
@@ -342,21 +342,21 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
Sm::Perforate { diameter, pitch, margin, pattern } => {
enter |= expr_row(ui, "ui.diameter", diameter);
enter |= expr_row(ui, "ui.perforate_pitch", pitch);
enter |= hint_row(ui, "Margin", margin, crate::i18n::tr_label("ui.perforate_margin_default"));
enter |= hint_row(ui, "Margin", margin, solidcraft_ui_text::tr_label("ui.perforate_margin_default"));
choice_row(ui, "Pattern", "sm_perforate_pattern", pattern, &PERFORATION_LABELS);
}
Sm::Hem { length, gap, flip } => {
form_row(ui, "Type", |ui| {
ui.label(crate::i18n::tr_label("Flat"));
ui.label(solidcraft_ui_text::tr_label("Flat"));
});
enter |= expr_row(ui, "Length", length);
enter |= hint_row(ui, "ui.sheet_gap", gap, crate::i18n::tr_label("from the rule"));
enter |= hint_row(ui, "ui.sheet_gap", gap, solidcraft_ui_text::tr_label("from the rule"));
crate::ui_kit::options(ui, "sheet_options", &mut [("Flip", flip)]);
}
Sm::Fold { angle, radius, position, flip } => {
choice_row(ui, "Bend Line Position", "sm_fold_pos", position, &FOLD_POSITION_LABELS);
enter |= expr_row(ui, "Angle", angle);
enter |= hint_row(ui, "Bend Radius", radius, crate::i18n::tr_label("from the rule"));
enter |= hint_row(ui, "Bend Radius", radius, solidcraft_ui_text::tr_label("from the rule"));
crate::ui_kit::options(ui, "sheet_options", &mut [("Flip", flip)]);
}
Sm::Path { radius } => {
@@ -365,14 +365,14 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
let lengths = path_lengths(app, inputs);
form_row(ui, "Flanges", |ui| {
ui.label(if lengths.is_empty() {
crate::i18n::tr_label("pick the path").to_string()
solidcraft_ui_text::tr_label("pick the path").to_string()
} else {
let each: Vec<String> = lengths.iter().map(|l| format!("{l:.1}")).collect();
format!("{} mm", each.join(" · "))
});
});
crate::ui_kit::caption(ui, "Flanges", crate::i18n::tr_label("set by the sketch's lines"));
enter |= hint_row(ui, "Bend Radius", radius, crate::i18n::tr_label("from the rule"));
crate::ui_kit::caption(ui, "Flanges", solidcraft_ui_text::tr_label("set by the sketch's lines"));
enter |= hint_row(ui, "Bend Radius", radius, solidcraft_ui_text::tr_label("from the rule"));
}
Sm::Unfold { .. } => {}
Sm::Convert { rule } => rule_combo(app, ui, "sm_conv_rule", rule),
@@ -411,7 +411,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
}
if *pick < rules.len()
&& !app.session.doc.sheet.rules.is_empty()
&& crate::ui_kit::button(ui, crate::i18n::tr_label("Delete rule"), crate::ui_kit::Button::Normal).clicked()
&& crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Delete rule"), crate::ui_kit::Button::Normal).clicked()
{
let n = rules.get(*pick).map(|r| r.name.clone()).unwrap_or_default();
match app.run("sheet.manage_rules", json!({"name": n, "delete": true})) {
@@ -467,12 +467,12 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel
});
}
ui.vertical(|ui| {
if crate::ui_kit::button(ui, crate::i18n::tr_label("Export DXF…"), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Export DXF…"), crate::ui_kit::Button::Normal).clicked() {
*status = Some(export_flat(app, v["body"].as_str().unwrap_or(""), "dxf"));
}
});
ui.vertical(|ui| {
if crate::ui_kit::button(ui, crate::i18n::tr_label("Export Bend Table…"), crate::ui_kit::Button::Normal).clicked() {
if crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Export Bend Table…"), crate::ui_kit::Button::Normal).clicked() {
*status = Some(export_flat(app, v["body"].as_str().unwrap_or(""), "csv"));
}
});
+29 -29
View File
@@ -7,7 +7,7 @@ use crate::Services;
use crate::dialogs::{apply_commands, for_feature as edit_dialog};
/// A 100 x 60 base flange on XY (2.5 mm steel).
fn plate(mode: crate::shell::LayoutMode) -> SolveApp {
fn plate(mode: crate::ui_kit::LayoutMode) -> SolveApp {
let mut s = Session::default();
s.execute("sketch.create", &json!({"plane": "XY", "name": "Base"})).unwrap();
s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [100, 60]})).unwrap();
@@ -21,7 +21,7 @@ fn plate(mode: crate::shell::LayoutMode) -> SolveApp {
/// dialog's height, angle and bend position; Edit Feature brings the edges back.
#[test]
fn flange_follows_the_picks() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
let sk = app.session.doc.features[0].id;
app.session.selection = vec![Sel::Profile { sketch: sk, index: 0 }];
@@ -95,7 +95,7 @@ fn a_click_on_the_bottom_edge_makes_an_edge_flange() {
/// Hem takes edges and an optional gap; rules make and pick a rule with its expressions.
#[test]
fn hem_and_rules() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.session.selection = edge_sel(&app.session, Vec3::new(50.0, 0.0, 2.5)).into_iter().collect();
@@ -133,7 +133,7 @@ fn hem_and_rules() {
#[test]
fn flat_exports_use_the_save_picker() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
use std::sync::{Arc, Mutex};
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
@@ -157,7 +157,7 @@ fn flat_exports_use_the_save_picker() {
#[test]
fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sketch.create", json!({"plane": "XY", "name": "Pocket"})).unwrap();
@@ -225,7 +225,7 @@ fn flat_pattern_shows_each_side_of_the_flat_size_whole() {
#[test]
fn flat_pattern_islands_fit_before_the_actual_command_panel() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sketch.create", json!({"plane": "XY", "name": "Retain"})).unwrap();
@@ -247,7 +247,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
let viewport = Rect::from_min_max(pos2(250.0, crate::toolbar::APP_BAR_HEIGHT + crate::toolbar::RIBBON_HEIGHT), pos2(1400.0, 832.0));
let browser = Rect::from_min_max(viewport.min, viewport.min + vec2(250.0, 450.0));
let history = Rect::from_min_max(pos2(viewport.left(), viewport.bottom() - 40.0), viewport.max);
if mode == crate::shell::LayoutMode::Floating {
if mode == crate::ui_kit::LayoutMode::Floating {
app.shell.content = Some(Rect::from_min_max(pos2(browser.right(), viewport.top()), pos2(viewport.right(), history.top())));
}
let ctx = crate::fonts::test_context();
@@ -286,7 +286,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
outlines.iter().flat_map(|p| &p.points).all(|p| p.x <= panel.left() - 19.0),
"both complete islands must remain exposed before the command panel: {panel:?}"
);
if mode == crate::shell::LayoutMode::Floating {
if mode == crate::ui_kit::LayoutMode::Floating {
assert!(
outlines.iter().flat_map(|p| &p.points).all(|p| !browser.contains(*p) && !history.contains(*p)),
"flat pattern fits clear of the shell cards"
@@ -297,7 +297,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
#[test]
fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch":"Base"})).unwrap();
app.run("sketch.create", json!({"plane":"XY","name":"Retain"})).unwrap();
@@ -329,7 +329,7 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
#[test]
fn pocket_export_button_uses_the_picker_and_preserves_depth_layers() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut h = crate::scenario::Harness::new();
h.call("ui.set", json!({"layout":mode}));
for (command, params) in [
@@ -366,7 +366,7 @@ fn pocket_export_button_uses_the_picker_and_preserves_depth_layers() {
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn bend_table_picker_writes_the_current_body() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20})).unwrap();
@@ -394,7 +394,7 @@ fn bend_table_picker_writes_the_current_body() {
/// made with it, and the dialog shows it the next time.
#[test]
fn rules_dialog_picks_the_corner_relief_shape() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut h = crate::scenario::Harness::new();
h.call("ui.set", json!({"layout":mode}));
h.app
@@ -447,7 +447,7 @@ fn rules_dialog_picks_the_corner_relief_shape() {
/// of the part made with it, and the dialog shows it the next time.
#[test]
fn rules_dialog_sets_how_bend_angles_read() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut h = crate::scenario::Harness::new();
h.call("ui.set", json!({"layout":mode}));
h.app.run("sheet.manage_rules", json!({"name": "AL2", "thickness": "2 mm", "bend_radius": "1 mm", "active": true})).unwrap();
@@ -551,7 +551,7 @@ fn the_rule_manager_relieves_a_corner_that_is_not_square() {
/// a shape and a size reach the feature, Edit Feature shows them again, and Rule takes them back.
#[test]
fn flange_dialog_picks_a_corner_relief_of_its_own() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut h = crate::scenario::Harness::new();
h.call("ui.set", json!({"layout":mode}));
h.app
@@ -627,7 +627,7 @@ fn flange_dialog_picks_a_corner_relief_of_its_own() {
#[test]
fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("document.units", json!({"units":"in"})).unwrap();
assert_eq!(super::pocket_label(&app, &json!({"side":"top","depth_mm":25.4*0.1234})).as_deref(), Some("Top pocket: 0.1234 in deep"));
@@ -640,7 +640,7 @@ fn pocket_depth_labels_follow_inches_without_changing_flat_data() {
/// brings them back and rebuilds the same flanges.
#[test]
fn edge_flanges_can_have_an_angle_and_a_height_of_their_own() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let edges: Vec<Sel> = [[50.0, 0.0, 2.5], [50.0, 60.0, 2.5], [0.0, 30.0, 2.5], [100.0, 30.0, 2.5]]
@@ -703,7 +703,7 @@ fn edge_flanges_can_have_an_angle_and_a_height_of_their_own() {
/// one flange command, one body and one undo step, and a bend with no height is refused.
#[test]
fn further_bends_in_the_flange_dialog_make_one_chained_flap() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sheet.flange", json!({"edges": [[0, 30, 2.5]], "height": 30})).unwrap();
@@ -777,7 +777,7 @@ fn add_bend_and_remove_bend_buttons_work() {
/// reaches the feature, and Edit Feature shows it again.
#[test]
fn flange_dialog_measures_the_height_from_the_chosen_datum() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut h = crate::scenario::Harness::new();
h.call("ui.set", json!({"layout":mode}));
h.app.run("sketch.create", json!({"plane": "XY", "name": "Base"})).unwrap();
@@ -901,7 +901,7 @@ fn convert_takes_the_fixed_face_and_the_bend_edges() {
/// and Edit Feature brings the edge and the path back.
#[test]
fn path_flange_dialog_follows_the_path() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sheet.flange", json!({"edges": [[0, 30, 2.5]], "height": 30})).unwrap();
@@ -949,7 +949,7 @@ fn path_flange_dialog_follows_the_path() {
// ---- flat pattern navigation ----
fn flat_app(preset: &str, mode: crate::shell::LayoutMode) -> SolveApp {
fn flat_app(preset: &str, mode: crate::ui_kit::LayoutMode) -> SolveApp {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap();
@@ -989,7 +989,7 @@ fn frame(app: &mut SolveApp, ctx: &egui::Context, time: &mut f64, mods: egui::Mo
/// cursor. The flat view never orbits.
#[test]
fn flat_pattern_pans_and_zooms_like_the_design_viewport() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
use egui::{Event, Modifiers, PointerButton};
let none = Modifiers::NONE;
let chords = [none, Modifiers::SHIFT, Modifiers::CTRL, Modifiers::ALT, Modifiers::SHIFT | Modifiers::ALT];
@@ -1094,7 +1094,7 @@ impl IntoNav for egui::PointerButton {
/// camera and selection alone.
#[test]
fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
use egui::{Event, Modifiers, PointerButton};
let none = Modifiers::NONE;
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
@@ -1136,7 +1136,7 @@ fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() {
/// Fit and Home frame the flat pattern (not the model), and leave the Design camera alone.
#[test]
fn fit_and_home_frame_the_flat_pattern() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let ctx = crate::fonts::test_context();
let mut app = flat_app("fusion", mode);
let mut t = 0.0;
@@ -1172,7 +1172,7 @@ fn fit_and_home_frame_the_flat_pattern() {
}
fn perforated_flat() -> (SolveApp, Dialog) {
let mut app = plate(crate::shell::LayoutMode::Floating);
let mut app = plate(crate::ui_kit::LayoutMode::Floating);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
app.run("sheet.perforate", json!({"diameter": 3, "pitch": 6})).unwrap();
let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap();
@@ -1251,7 +1251,7 @@ fn flat_perforations_cull_offscreen_and_subpixel_holes() {
#[test]
fn hem_handle_runs_along_the_hem_to_its_tip() {
for flipped in [false, true] {
let mut app = plate(crate::shell::LayoutMode::Floating);
let mut app = plate(crate::ui_kit::LayoutMode::Floating);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let body = app.session.model.state().sheets[0].body.clone();
app.session.selection = vec![edge_sel(&app.session, Vec3::new(50.0, 0.0, 2.5)).unwrap()];
@@ -1296,7 +1296,7 @@ fn inch_sheet_rule_edit_preserves_stored_lengths_and_dimensionless_k_factor() {
#[test]
fn perforate_toolbar_opens_without_running_empty_parameters() {
for mode in [crate::shell::LayoutMode::Floating, crate::shell::LayoutMode::Docked] {
for mode in [crate::ui_kit::LayoutMode::Floating, crate::ui_kit::LayoutMode::Docked] {
let mut app = plate(mode);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let features = app.session.doc.features.len();
@@ -1328,7 +1328,7 @@ fn perforate_toolbar_opens_without_running_empty_parameters() {
#[test]
fn perforate_ok_targets_the_picked_sheet_and_updates_flat_data() {
for face in [false, true] {
let mut app = plate(crate::shell::LayoutMode::Docked);
let mut app = plate(crate::ui_kit::LayoutMode::Docked);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let body = app.session.model.state().sheets[0].body.clone();
app.session.selection =
@@ -1349,7 +1349,7 @@ fn perforate_ok_targets_the_picked_sheet_and_updates_flat_data() {
#[test]
fn perforate_edit_restores_dimensions_pattern_and_target() {
let mut app = plate(crate::shell::LayoutMode::Docked);
let mut app = plate(crate::ui_kit::LayoutMode::Docked);
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
let body = app.session.model.state().sheets[0].body.clone();
app.run("sheet.perforate", json!({"body": body, "diameter": "4 mm", "pitch": "9 mm", "margin": "5 mm", "pattern": "square"})).unwrap();
@@ -1366,7 +1366,7 @@ fn perforate_edit_restores_dimensions_pattern_and_target() {
#[test]
fn flat_pattern_navigation_slots_hide_orbit_controls_and_keep_fit_home() {
let ctx = crate::fonts::test_context();
let mut app = flat_app("fusion", crate::shell::LayoutMode::Floating);
let mut app = flat_app("fusion", crate::ui_kit::LayoutMode::Floating);
app.preferences.navigation_bar = serde_json::from_value(json!({
"slots":["orbit","spin","look_at","projection","visual_style","pan","zoom","fit","home","display"]
}))
+1 -1
View File
@@ -174,7 +174,7 @@ fn equation_curve_window_gives_the_variable_its_ranges_unit() {
*start = "a".into();
*end = "3 * a".into();
h.settle();
let handle = format!("dialog:{}", crate::i18n::tr("ui.equation_variable_unit"));
let handle = format!("dialog:{}", solidcraft_ui_text::tr("ui.equation_variable_unit"));
h.step(&json!({"click": {"handle": handle}})).unwrap();
h.settle();
let Some(Kind::SketchModify(Mod::Equation { range_unit, .. })) = h.app.dialog.as_ref().map(|d| &d.kind) else { panic!("no equation window") };
+42 -9
View File
@@ -3,6 +3,7 @@
//! Fair Surface, Match Surface and Split Face. Each has a live preview and Edit Feature.
use crate::dialogs::put;
use crate::ui_kit::BackingTooltipExt as _;
use crate::ui_kit::{choice_row, expr_row, hint_row};
use serde_json::{Map, Value, json};
use solidcore_record::Vec3;
@@ -150,6 +151,38 @@ impl Surf {
}
}
/// The one control that chooses a continuity order, wherever one is chosen: a dropdown of
/// `allowed` (what the construction builds, `Construction::offered`). Its labels are the orders'
/// plain words and its hover text a line per order naming its code, both from the kernel's one
/// table of orders; a value not in `allowed` shows the first. Tokens: `row, dropdown`.
pub(crate) fn continuity_row(ui: &mut egui::Ui, label: &str, id: &str, value: &mut Continuity, allowed: &[Continuity]) -> egui::Response {
continuity_row_explained(ui, label, id, value, allowed, &continuity_tip(allowed))
}
/// [`continuity_row`] whose hover text says what each order builds there (`tip`, which names
/// each order's code), for a choice whose orders stand for constructions (a fillet's corners).
pub(crate) fn continuity_row_explained(
ui: &mut egui::Ui,
label: &str,
id: &str,
value: &mut Continuity,
allowed: &[Continuity],
tip: &str,
) -> egui::Response {
let labels: Vec<&str> = allowed.iter().map(|g| g.label_id()).collect();
let mut index = allowed.iter().position(|g| g == value).unwrap_or(0);
let response = choice_row(ui, label, id, &mut index, &labels);
if let Some(g) = allowed.get(index) {
*value = *g;
}
response.backing_on_hover_text(tip)
}
/// The hover text of a continuity choice: each order's own line, which names its code.
pub(crate) fn continuity_tip(allowed: &[Continuity]) -> String {
allowed.iter().map(|g| solidcraft_ui_text::tr(g.tip_id())).collect::<Vec<_>>().join("\n")
}
/// What each dialog's continuity rows offer: the orders its construction builds, from the
/// kernel's table, so an order a construction learns shows up by itself.
fn patch_orders() -> Vec<Continuity> {
@@ -160,7 +193,7 @@ fn patch_orders() -> Vec<Continuity> {
/// faces meet each other with different curvatures (tangent only: no surface smooth at the corner
/// has both curvatures there).
fn patch_continuity_tip() -> String {
format!("{}\n{}", ui_kit::continuity_tip(&patch_orders()), crate::i18n::tr("ui.patch_continuity_corner_tip"))
format!("{}\n{}", continuity_tip(&patch_orders()), solidcraft_ui_text::tr("ui.patch_continuity_corner_tip"))
}
fn blend_orders() -> Vec<Continuity> {
@@ -262,14 +295,14 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Surf) -> bool {
form_row(ui, "ui.fair_iterations", |ui| {
ui_kit::number_field(ui, iterations, 1..=1000, 1.0, "");
});
enter |= hint_row(ui, "Tolerance", tolerance, crate::i18n::tr_label("ui.fair_tolerance_default"));
enter |= hint_row(ui, "Tolerance", tolerance, solidcraft_ui_text::tr_label("ui.fair_tolerance_default"));
ui_kit::options(ui, "fair_options", &mut [("ui.fair_keep_edges", keep_edges)]);
}
Surf::Match { order } => {
choice_row(ui, "Continuity", "match_order", order, &MATCH_CONTINUITY.map(|(label, _)| label));
}
Surf::Stitch { tolerance } => {
enter |= hint_row(ui, "Tolerance", tolerance, crate::i18n::tr("ui.stitch_tolerance_hint"));
enter |= hint_row(ui, "Tolerance", tolerance, solidcraft_ui_text::tr("ui.stitch_tolerance_hint"));
}
Surf::Thicken { thickness, symmetric, operation } => {
enter |= expr_row(ui, "Thickness", thickness);
@@ -319,23 +352,23 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Surf) -> bool {
ui_kit::options(ui, "untrim_options", &mut [("Restore outer boundary", outer), ("In place (surfaces)", in_place)]);
}
Surf::Patch { continuity } => {
ui_kit::continuity_row_explained(ui, "Continuity", "pa_cont", continuity, &patch_orders(), &patch_continuity_tip());
continuity_row_explained(ui, "Continuity", "pa_cont", continuity, &patch_orders(), &patch_continuity_tip());
}
Surf::Blend { continuity, bulge, stitch } => {
let [from, to] = continuity;
let [from_bulge, to_bulge] = bulge;
ui_kit::continuity_row(ui, "Start continuity", "bl_cont0", from, &blend_orders());
continuity_row(ui, "Start continuity", "bl_cont0", from, &blend_orders());
form_row(ui, "Start bulge", |ui| {
ui_kit::scalar_field(ui, from_bulge, -0.9..=3.0);
});
ui_kit::continuity_row(ui, "End continuity", "bl_cont1", to, &blend_orders());
continuity_row(ui, "End continuity", "bl_cont1", to, &blend_orders());
form_row(ui, "End bulge", |ui| {
ui_kit::scalar_field(ui, to_bulge, -0.9..=3.0);
});
ui_kit::options(ui, "blend_options", &mut [("Stitch to both bodies", stitch)]);
}
Surf::Boundary { continuity, join } => {
ui_kit::continuity_row(ui, "Continuity", "bo_cont", continuity, &boundary_orders());
continuity_row(ui, "Continuity", "bo_cont", continuity, &boundary_orders());
form_row(ui, "Operation", |ui| {
let mut op = usize::from(*join);
combo(ui, "bo_op", &BOUNDARY_OPS, &mut op);
@@ -1182,7 +1215,7 @@ mod tests {
}
assert_eq!(blend_orders().last(), Some(&Continuity::G3), "a blend builds up to G3 today");
for lang in ["en", "ar", "he", "ja", "zh", "de", "ko"] {
let locale = crate::i18n::Locale::for_language(lang);
let locale = solidcraft_ui_text::Locale::for_language(lang);
for g in Continuity::ALL {
let (label, tip) = (locale.text(g.label_id()), locale.text(g.tip_id()));
assert!(label != g.label_id() && tip != g.tip_id(), "{lang}: {g:?} has no text");
@@ -1190,7 +1223,7 @@ mod tests {
assert!(tip.contains(g.code()), "{lang}: the {g:?} hover text names its code: {tip}");
}
}
assert_eq!(ui_kit::continuity_tip(&blend_orders()).lines().count(), 4);
assert_eq!(continuity_tip(&blend_orders()).lines().count(), 4);
// The patch's hover text adds what it does at a corner whose faces' curvatures differ.
let tip = patch_continuity_tip();
assert_eq!(tip.lines().count(), patch_orders().len() + 1, "{tip}");
+3 -3
View File
@@ -13,9 +13,9 @@ use solidcraft_engine::doc::{
use solidcraft_engine::{Sel, Session};
use crate::dialogs::{Dialog, Kind, combo, edge_sel, face_sel, form_row, object_point, plane_sel, put, set_object_accepting};
use crate::i18n::tr;
use crate::selection::{Accept, EDGES, FACES, PLANES, SelInput};
use crate::ui_kit::{choice_row, expr_row, hint_row, text_row};
use solidcraft_ui_text::tr;
/// Common metric pitch presets besides the coarse one, not an exhaustive catalogue of standard
/// diameter/pitch pairs; the current pitch is included separately, custom pairs remain editable.
@@ -179,7 +179,7 @@ pub(crate) fn hole_rows(
(false, true) => "thread.drill_range",
(false, false) => "thread.drill_range_custom",
};
let tip = crate::i18n::format(id, &args);
let tip = solidcraft_ui_text::format(id, &args);
let mut text = drill.clone().unwrap_or_else(|| default.clone());
let (submitted, changed) = crate::ui_kit::text_field_tip(ui, &mut text, &default, &tip);
if changed {
@@ -434,7 +434,7 @@ pub(crate) fn rows(
if !f.sizes().is_empty() {
size_rows(ui, f, designation, suggested);
}
let hint = crate::i18n::format("thread.fits_the_face", &[("example", &example(f))]);
let hint = solidcraft_ui_text::format("thread.fits_the_face", &[("example", &example(f))]);
let (submitted, changed) = text_row(ui, "Designation", designation, &hint);
enter |= submitted;
if let Some(at) = crate::scenario::handle_at("dialog:Designation") {
+7 -7
View File
@@ -127,10 +127,10 @@ pub(crate) fn stop_text(v: &Value) -> Option<String> {
// The values come from the engine: a missing key is an empty name, never a panic.
let name = |m: &serde_json::Map<String, Value>, key: &str| m.get(key).and_then(Value::as_str).unwrap_or_default().to_string();
if let Some(l) = v["stopped_by_loop"].as_object() {
return Some(crate::i18n::format("trace.stopped_by_loop", &[("sample", &at), ("joint", &name(l, "joint"))]));
return Some(solidcraft_ui_text::format("trace.stopped_by_loop", &[("sample", &at), ("joint", &name(l, "joint"))]));
}
let c = v["stopped_by_contact"].as_object()?;
Some(crate::i18n::format("trace.stopped_by_contact", &[("sample", &at), ("a", &name(c, "a")), ("b", &name(c, "b"))]))
Some(solidcraft_ui_text::format("trace.stopped_by_contact", &[("sample", &at), ("a", &name(c, "a")), ("b", &name(c, "b"))]))
}
/// The dialog's rows; true when Enter was pressed in a value.
@@ -191,9 +191,9 @@ pub fn rows(app: &mut crate::SolveApp, ui: &mut egui::Ui, f: &mut TraceForm, inp
let (text, tone) = match r {
Ok(v) => match stop_text(v) {
Some(why) => (why, crate::ui_kit::StatusTone::Warning),
None => (crate::i18n::format("trace.points", &[("count", &v["samples"].to_string())]), crate::ui_kit::StatusTone::Dim),
None => (solidcraft_ui_text::format("trace.points", &[("count", &v["samples"].to_string())]), crate::ui_kit::StatusTone::Dim),
},
Err(e) => (crate::i18n::tr_label(e).to_string(), crate::ui_kit::StatusTone::Dim),
Err(e) => (solidcraft_ui_text::tr_label(e).to_string(), crate::ui_kit::StatusTone::Dim),
};
crate::ui_kit::status_line(ui, &text, tone);
}
@@ -235,15 +235,15 @@ mod shared_status_baseline_tests {
}
assert_eq!(
stop_text(&json!({"stopped_at":3,"stopped_by_loop":{"joint":"Hinge"},"stopped_by_contact":{"a":"A","b":"B"}})),
Some(crate::i18n::format("trace.stopped_by_loop", &[("sample", "3"), ("joint", "Hinge")]))
Some(solidcraft_ui_text::format("trace.stopped_by_loop", &[("sample", "3"), ("joint", "Hinge")]))
);
assert_eq!(
stop_text(&json!({"stopped_at":4,"stopped_by_contact":{"a":"A","b":"B"}})),
Some(crate::i18n::format("trace.stopped_by_contact", &[("sample", "4"), ("a", "A"), ("b", "B")]))
Some(solidcraft_ui_text::format("trace.stopped_by_contact", &[("sample", "4"), ("a", "A"), ("b", "B")]))
);
assert_eq!(
stop_text(&json!({"stopped_at":0,"stopped_by_contact":{"a":1,"b":null}})),
Some(crate::i18n::format("trace.stopped_by_contact", &[("sample", "0"), ("a", ""), ("b", "")]))
Some(solidcraft_ui_text::format("trace.stopped_by_contact", &[("sample", "0"), ("a", ""), ("b", "")]))
);
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ pub(crate) fn progress_text(work: Option<(&str, usize, usize)>) -> String {
"dxf_write" => "dxf.progress.write",
_ => "dxf.progress.meshes",
};
format!("{}: {done}/{total}", crate::i18n::tr_label(label))
format!("{}: {done}/{total}", solidcraft_ui_text::tr_label(label))
}
#[cfg(test)]
+8 -8
View File
@@ -163,7 +163,7 @@ impl DimensionDraft {
/// of its id, and without the `value:` field tag, since the box is the value
/// ("Sketch Dimension: `mm` is a unit name, so it cannot be a parameter name").
fn shown_error(id: &str, e: &str) -> String {
let label = crate::i18n::tr_label(crate::command_label(id));
let label = solidcraft_ui_text::tr_label(crate::command_label(id));
crate::friendly_error(id, e).replacen(&format!("{label}: value: "), &format!("{label}: "), 1)
}
@@ -226,7 +226,7 @@ pub fn begin_dimension(app: &mut SolveApp, entities: Vec<String>, at: Vec2) -> R
ellipse_as_before(draft.half_axes, &mut params);
AXIS.with(|axis| *axis.borrow_mut() = None);
app.run("sketch.dimension", params)?;
app.set_status(crate::i18n::tr("sketch.dimension_added_driven"), false);
app.set_status(solidcraft_ui_text::tr("sketch.dimension_added_driven"), false);
return Ok(());
}
EDIT.with(|edit| {
@@ -753,7 +753,7 @@ fn label_marked(k: &ConstraintKind, radius_mark: bool, expr: Option<&str>, drive
let plain = expr.is_none_or(|e| {
let e = e.trim();
let e = ["mm", "cm", "in", "ft", "m", "deg", "rad", "°"].iter().find_map(|unit| e.strip_suffix(unit)).unwrap_or(e);
crate::prefs::parse_number(e).is_some()
crate::ui_kit::parse_number(e).is_some()
});
let text = if plain { num } else { format!("fx: {num}") };
if driven { format!("({text})") } else { text }
@@ -1166,8 +1166,8 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) {
Container::show_backing(ui, frame, |ui| {
crate::ui_kit::scope(ui, |ui| {
ui.set_max_width(popup_width);
crate::ui_kit::text(ui, &crate::i18n::format("dialog.dimension_name", &[("name", &e.param)]), crate::ui_kit::Text::Inline)
.backing_on_hover_text(crate::i18n::tr("dialog.dimension_insert_hint"));
crate::ui_kit::text(ui, &solidcraft_ui_text::format("dialog.dimension_name", &[("name", &e.param)]), crate::ui_kit::Text::Inline)
.backing_on_hover_text(solidcraft_ui_text::tr("dialog.dimension_insert_hint"));
if !e.owned {
// The whole value is selected before the field reads its state, every frame
// until the person takes it over, so typing replaces it (#11204). Only on the
@@ -1266,9 +1266,9 @@ fn settle(app: &mut SolveApp, mut e: Edit, done: Option<bool>, escaped: bool) {
// The sentence comes from the catalog, so it reads in the interface language (#8863).
let named = reply.get("renamed").and_then(|r| r.get("to")).and_then(serde_json::Value::as_str);
let status = match (reply.get("defined").and_then(serde_json::Value::as_str), named) {
(Some(name), _) => crate::i18n::format("sketch.dimension_applied_defined", &[("name", name)]),
(None, Some(name)) => crate::i18n::format("sketch.dimension_applied_renamed", &[("name", name)]),
(None, None) => crate::i18n::tr("sketch.dimension_applied").to_string(),
(Some(name), _) => solidcraft_ui_text::format("sketch.dimension_applied_defined", &[("name", name)]),
(None, Some(name)) => solidcraft_ui_text::format("sketch.dimension_applied_renamed", &[("name", name)]),
(None, None) => solidcraft_ui_text::tr("sketch.dimension_applied").to_string(),
};
app.set_status(status, false);
}
+6 -6
View File
@@ -229,17 +229,17 @@ pub(super) fn show(app: &mut SolveApp, ctx: &egui::Context, e: &mut Edit, at: Po
ui.set_width(width);
// The parameter's name, then the check and the cross.
let (title, _) = ui.allocate_exact_size(egui::vec2(width, t.control_height), egui::Sense::hover());
let name = crate::i18n::format("dialog.dimension_name", &[("name", &e.param)]);
let name = solidcraft_ui_text::format("dialog.dimension_name", &[("name", &e.param)]);
ui.scope_builder(egui::UiBuilder::new().max_rect(title).layout(egui::Layout::left_to_right(egui::Align::Center)), |ui| {
crate::ui_kit::text(ui, &name, crate::ui_kit::Text::Inline).on_hover_text(crate::i18n::tr("dialog.dimension_insert_hint"));
crate::ui_kit::text(ui, &name, crate::ui_kit::Text::Inline).on_hover_text(solidcraft_ui_text::tr("dialog.dimension_insert_hint"));
});
let side = t.control_height;
let cancel = Rect::from_min_size(egui::pos2(title.right() - side, title.top()), egui::vec2(side, side));
let ok = cancel.translate(egui::vec2(-(side + t.space_2), 0.0));
if button(ui, ok, "dimwin:ok", crate::i18n::tr("ui.heading_ok"), "finish") {
if button(ui, ok, "dimwin:ok", solidcraft_ui_text::tr("ui.heading_ok"), "finish") {
answer = Some(true);
}
if button(ui, cancel, "dimwin:cancel", crate::i18n::tr("ui.cancel"), "close") {
if button(ui, cancel, "dimwin:cancel", solidcraft_ui_text::tr("ui.cancel"), "close") {
answer = Some(false);
escaped = true;
}
@@ -261,9 +261,9 @@ pub(super) fn show(app: &mut SolveApp, ctx: &egui::Context, e: &mut Edit, at: Po
let can_step = step(&e.expr, 1.0).is_some();
let tip = |id: &str| {
if can_step {
crate::i18n::format(id, &[("step", &step_text)])
solidcraft_ui_text::format(id, &[("step", &step_text)])
} else {
crate::i18n::tr("dialog.dimension_step_expression").to_string()
solidcraft_ui_text::tr("dialog.dimension_step_expression").to_string()
}
};
let up = crate::ui_kit::compact_button(ui, crate::ui_kit::MOVE_UP, &tip("dialog.dimension_step_up"), can_step, false);
@@ -394,7 +394,7 @@ fn the_sketch_page_switches_dimension_values_to_a_window() {
let mut h = Harness::new();
h.call("ui.prefs", json!({"open": "Sketch"}));
h.frames(4);
let handle = format!("dialog:{}:{}", crate::i18n::tr("prefs.dimension_values"), crate::i18n::tr("prefs.dimension_values_window"));
let handle = format!("dialog:{}:{}", solidcraft_ui_text::tr("prefs.dimension_values"), solidcraft_ui_text::tr("prefs.dimension_values_window"));
crate::prefs::reveal_preference(&mut h, &handle);
h.step(&json!({"click":{"handle": handle}})).unwrap();
h.frames(2);
@@ -1970,8 +1970,8 @@ fn editing_a_dimension_to_a_definition_names_its_parameter() {
/// the parameter name kept as typed.
#[test]
fn the_status_after_a_dimension_is_in_the_interface_language() {
let pl = crate::i18n::Locale::for_language("pl");
let en = crate::i18n::Locale::for_language("en");
let pl = solidcraft_ui_text::Locale::for_language("pl");
let en = solidcraft_ui_text::Locale::for_language("en");
let status = |h: &Harness| h.app.status.as_ref().map(|s| (s.0.clone(), s.2));
let polish = |mut h: Harness| {
h.app.preferences.language = "pl".into();
+17 -12
View File
@@ -5,6 +5,7 @@
//! Closing a design with unsaved changes asks first, and so does quitting: one "save changes?"
//! per design with unsaved changes ([`request_quit`]).
use crate::i18n::LocaleNames as _;
use crate::ui_kit::BackingTooltipExt as _;
use crate::ui_kit::{Container, Surface};
use egui::{FontId, Rect, Sense, pos2, vec2};
@@ -874,8 +875,8 @@ fn tab_paths(app: &SolveApp) -> Vec<Option<String>> {
/// the full name above when the tab cut it short (#5868), and below, that a saved design has
/// changes not saved yet (`dirty`), which the bullet after a cut name no longer shows (#11408).
fn tab_tooltip(name: &str, path: Option<&str>, cut: bool, dirty: bool) -> String {
let place = path.map_or_else(|| crate::i18n::tr_label("Not saved yet").to_string(), crate::home::friendly_path);
let place = if dirty && path.is_some() { format!("{place}\n{}", crate::i18n::tr("document.tab_unsaved_changes")) } else { place };
let place = path.map_or_else(|| solidcraft_ui_text::tr_label("Not saved yet").to_string(), crate::home::friendly_path);
let place = if dirty && path.is_some() { format!("{place}\n{}", solidcraft_ui_text::tr("document.tab_unsaved_changes")) } else { place };
if cut { format!("{name}\n{place}") } else { place }
}
@@ -996,7 +997,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
.map(|(i, (name, dirty))| {
let shown_name = match &typed {
Some(text) if i == selected => text.clone(),
_ => crate::i18n::Locale::for_language(&app.preferences.language).given_name(name),
_ => solidcraft_ui_text::Locale::for_language(&app.preferences.language).given_name(name),
};
let label = format!("{shown_name}{}", if *dirty { " •" } else { "" });
let text = ui.painter().layout_no_wrap(label, crate::fonts::semibold(crate::theme::Tokens::get().font_button), t.app_bar_text).size().x;
@@ -1084,7 +1085,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
let lit = resp.hovered() || held.is_some_and(|(from, _)| from == i);
tab_shape(ui.painter(), &t, tr, active, lit || resp.has_focus());
let ink = if active || lit { t.app_bar_text } else { t.app_bar_text.gamma_multiply(0.7) };
let name_display = crate::i18n::Locale::for_language(&app.preferences.language).given_name(name);
let name_display = solidcraft_ui_text::Locale::for_language(&app.preferences.language).given_name(name);
let label = format!("{name_display}{}", if *dirty { " •" } else { "" });
let font = if active {
crate::fonts::semibold(crate::theme::Tokens::get().font_button)
@@ -1125,7 +1126,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
menu = Some((i, ui.ctx().pointer_interact_pos().unwrap_or(tr.left_bottom())));
}
// Named apart from a dialog's Close: egui 0.37 names an unnamed widget after its tooltip.
xresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, show_close, crate::i18n::tr("ui.close_tab")));
xresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, show_close, solidcraft_ui_text::tr("ui.close_tab")));
if xresp.backing_on_hover_text("Close").clicked() {
shut = Some(i);
} else if (if held.is_some() { resp } else { resp.backing_on_hover_text(tip) }).clicked() {
@@ -1155,7 +1156,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
t.app_bar_text,
);
let id = egui::Id::unique("sc_doc_more");
if mresp.clone().backing_on_hover_text(crate::i18n::tr_label("All open designs")).clicked() {
if mresp.clone().backing_on_hover_text(solidcraft_ui_text::tr_label("All open designs")).clicked() {
ui.ctx().data_mut(|d| d.insert_temp(id, !d.get_temp::<bool>(id).unwrap_or(false)));
}
if ui.ctx().data(|d| d.get_temp::<bool>(id).unwrap_or(false)) {
@@ -1189,7 +1190,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
crate::context_menu::menu_frame(ui, |ui| {
let mut chosen = None;
for (id, label) in items {
let mut item = crate::context_menu::Item::action(id, label, "");
let mut item = crate::ui_kit::Item::action(id, label, "");
if id == "ui.closeTab" && i == app.docs.active {
item = item.key("Ctrl+W");
}
@@ -1290,7 +1291,7 @@ fn renaming_tab(app: &SolveApp) -> bool {
/// Starts editing the selected design's name in its tab, with the name as shown selected.
fn start_tab_rename(app: &mut SolveApp, name: &str, tab: Rect) {
let text = crate::i18n::Locale::for_language(&app.preferences.language).given_name(name);
let text = solidcraft_ui_text::Locale::for_language(&app.preferences.language).given_name(name);
app.menu.rename = Some(crate::context_menu::Rename {
what: crate::context_menu::RenameWhat::Component(0),
text,
@@ -1363,7 +1364,7 @@ pub enum Answer {
/// The "save changes?" prompt for a tab being closed, or for each unsaved design when quitting.
pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
let _language = crate::i18n::scope(&app.preferences.language);
let _language = solidcraft_ui_text::scope(&app.preferences.language);
quit_step(app);
// Saved, it asks the Capture Position question first (#9736): that question is the one to
// answer meanwhile, not this one again.
@@ -1397,10 +1398,10 @@ pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
if enter {
answer = Some(choice);
}
let title = crate::i18n::format("document.save_changes", &[("name", &name_display)]);
let title = solidcraft_ui_text::format("document.save_changes", &[("name", &name_display)]);
let (closed, action) = ui_kit::Confirm::new(egui::Id::unique("sc_close_prompt"), "status_warning", &title)
.tone(ui_kit::Tone::Warning)
.actions(crate::i18n::tr("ui.save"), crate::i18n::tr("ui.cancel"), Some(crate::i18n::tr("ui.don_t_save")))
.actions(solidcraft_ui_text::tr("ui.save"), solidcraft_ui_text::tr("ui.cancel"), Some(solidcraft_ui_text::tr("ui.don_t_save")))
.secondary_left()
.selected(match choice {
Answer::Save => ui_kit::Action::Ok,
@@ -1408,7 +1409,11 @@ pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
Answer::Cancel => ui_kit::Action::Cancel,
})
.show(ctx, true, |ui| {
ui_kit::text(ui, &crate::i18n::format("document.unsaved_changes", &[("name", &name_display)]), ui_kit::Text::Report("unsaved", false));
ui_kit::text(
ui,
&solidcraft_ui_text::format("document.unsaved_changes", &[("name", &name_display)]),
ui_kit::Text::Report("unsaved", false),
);
});
answer = match action {
ui_kit::Action::Ok => Some(Answer::Save),

Some files were not shown because too many files have changed in this diff Show More