Compare commits

...

18 Commits

Author SHA1 Message Date
jyviko ff90f2a7f0 Theme: format the field contrast test (#13304)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 05:46:37 -05:00
jyviko 8c4fd59e4c Theme: the icon lint and the docs read palettes.rs at crates/theme (#13304)
lint_icons.py compared the icon review page with assets/styles/ui/palettes.rs, which moved into
solidcraft-theme; it reads crates/theme/src/palettes.rs now (cargo xtask icons passes again), and
the READMEs, docs and test comments name the new path. set_custom and scoped_tokens say they set
tokens only, so a host with an icon renderer sets its palette too; the field contrast test applies
each built-in theme with set_file_palette, as the app does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 05:46:37 -05:00
jyviko bb38419b11 Shared UI crates: the token contract as a shared crate, solidcraft-theme (#13304)
Tokens with SolidCraft's light and dark values in code (palettes.rs moves in from
assets/styles/ui, with the eight primary strokes it shared with the icon artwork written as
icon_strokes), colour vision, TabAccent, contrast checks, apply/visuals and the per-thread and
per-context theme state move into crates/theme, class Shared. It holds no data files and does not
depend on theme files or icon roles; with nothing set, components draw with the built-in tokens.

The theme system stays in ui-egui's theme module: the custom-theme glue (prepared theme, tab
targets and their fade, blur intent), publish_tab (was TabAccent::publish, which now stores its
result through TabAccent::set_current and set_published), set_file_palette, ViewportTheme.
ui-egui's build script no longer generates the primary-stroke constants; an icon test holds the
crate's literals equal to palette.toml's. Call sites use solidcraft_theme paths with no
re-export (one rename pass). Tokens, theme files and rendered pixels are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 05:46:37 -05:00
jyviko 0e23fdd570 Theme: the icon palette leaves Tokens; the icon renderer owns it (#13304)
A theme file's icon palette was a field of Tokens, so the token set depended on the icon roles
ui-egui's build script generates. It is now the icon renderer's per-thread state
(icon_svg::set_theme_palette), set alongside the theme's palette by theme::set_file_palette, and
theme_files resolves it with resolve_icon_palette (the same overlay merge as the tokens) and
exports it as an argument. Rendered pixels, theme files and exports are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUXeKQRurMYJST9eSZWKzW
2026-11-08 05:46:20 -05:00
jyviko bcd63adb98 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 00:38:43 -05:00
jyviko a9d6c7d810 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 00:38:43 -05:00
jyviko ec36c0b04d 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 00:02:22 -05:00
jyviko e2664ea368 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-07 23:52:35 -05:00
jyviko fe45f81f56 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-07 23:46:44 -05:00
jyviko 6e8165674e 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-07 23:46:44 -05:00
jyviko e3caced67b 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-07 23:46:44 -05:00
jyviko d996441241 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-07 23:46:44 -05:00
jyviko 8a40998c2a 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-07 23:46:44 -05:00
jyviko 924585be0a 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-07 23:26:23 -05:00
jyviko b2c0fd71d0 Shared UI crates, tranche 1: merge #12936 and #12938 for the batch 2026-11-07 22:46:18 -05:00
jyviko 2605261665 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-07 22:46:17 -05:00
jyviko 397bbf38c7 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-07 22:46:16 -05:00
jyviko 4db733ec68 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-07 22:46:16 -05:00
393 changed files with 8614 additions and 6624 deletions
-1
View File
@@ -201,7 +201,6 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/windows/solidcraft.wxs | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-dark.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/gruvbox-light.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solidcraft-95.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
Generated
+43 -2
View File
@@ -4096,7 +4096,9 @@ dependencies = [
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-space-mouse",
"solidcraft-theme",
"solidcraft-ui-egui",
"solidcraft-ui-text",
"ureq",
"windows-sys 0.61.2",
"winit",
@@ -4123,6 +4125,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 +4147,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-derive",
"solidcraft-geom",
"solidcraft-kernel",
@@ -4173,6 +4180,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"solidcore-record",
"solidcraft-color",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-io",
@@ -4207,6 +4215,7 @@ dependencies = [
"serde",
"serde_json",
"solidcore-record",
"solidcraft-color",
"solidcraft-engine",
"solidcraft-geom",
"solidcraft-io",
@@ -4234,6 +4243,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
@@ -4293,6 +4303,7 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-geom",
"web-time",
"weezl",
@@ -4345,6 +4356,16 @@ dependencies = [
name = "solidcraft-testkit"
version = "0.40.0-rc.3"
[[package]]
name = "solidcraft-theme"
version = "0.40.0-rc.3"
dependencies = [
"egui",
"serde",
"serde_json",
"solidcraft-color",
]
[[package]]
name = "solidcraft-ui-egui"
version = "0.40.0-rc.3"
@@ -4363,20 +4384,39 @@ dependencies = [
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-color",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-space-mouse",
"solidcraft-theme",
"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 +4429,7 @@ dependencies = [
"serde_json",
"solidcraft-engine",
"solidcraft-ui-egui",
"solidcraft-ui-text",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+4
View File
@@ -28,6 +28,10 @@ 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-theme = { path = "crates/theme" }
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());
+2
View File
@@ -34,6 +34,8 @@ 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-theme = { workspace = true }
solidcraft-space-mouse = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
egui = { workspace = true }
+3 -2
View File
@@ -468,7 +468,7 @@ fn case_variants(exts: &[&str]) -> Vec<String> {
fn content_ui(app: &mut SolveApp, ui: &mut egui::Ui) {
let content = ui.ctx().content_rect();
let above = egui::Rect::from_min_max(ui.max_rect().min, egui::pos2(ui.max_rect().right(), content.top()));
ui.painter().rect_filled(above, 0.0, solidcraft_ui_egui::theme::Tokens::get().app_bar);
ui.painter().rect_filled(above, 0.0, solidcraft_theme::Tokens::get().app_bar);
ui.scope_builder(egui::UiBuilder::new().max_rect(content), |ui| app.ui(ui));
}
@@ -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
-1
View File
@@ -18,7 +18,6 @@ assets/
ui/source/ # Rust viewport glyphs, markers and navigation
branding/web/source/og.png # shared social preview card
branding/logo/ # print/web logo files; guide in branding/README.md
styles/ui/palettes.rs # light/dark defaults
styles/web/loading.css # web loading/canvas styles
appearances/library.rs # built-in visual presets
materials/library.rs # physical defaults and colours
+1 -1
View File
@@ -38,7 +38,7 @@ Other colours, all already used in the project:
| Colour | Hex | Where it comes from |
|---|---|---|
| Wordmark blue | `#15499e` | loading logo `--word` |
| Interface accent | `#2678da` | light theme `accent` token (`assets/styles/ui/palettes.rs`) |
| Interface accent | `#2678da` | light theme `accent` token (`crates/theme/src/palettes.rs`) |
| Light background | `#e8ebf0` | web loading screen (`assets/styles/web/loading.css`) |
| Dark background | `#1f232b` | dark theme `file_background` token |
| Black / white | `#000000` / `#ffffff` | one-colour versions only |
+1 -1
View File
@@ -81,7 +81,7 @@ for message in icon_palette.validation_errors(pal, names | set(al)):
err(d + '/palette.toml', message)
# the review page lies its icons on the real app surfaces: its colours must be the app's tokens
def app_tokens():
src = open(os.path.join(d, '..', '..', '..', 'styles', 'ui', 'palettes.rs'), encoding='utf-8').read()
src = open(os.path.join(d, '..', '..', '..', '..', 'crates', 'theme', 'src', 'palettes.rs'), encoding='utf-8').read()
lo, hi = src.index('pub const fn light'), src.index('pub const fn dark')
light_src, dark_src = src[lo:hi], src[hi:]
def pick(block, name, dark):
@@ -1,4 +1,4 @@
/* App surfaces (keep in sync with assets/styles/ui/palettes.rs; lint_icons.py checks): --card is the ribbon surface `toolbar` the icons lie on, --bg the page (`panel` in light, `viewport_top` in dark), --line `border`, --fg `text`, --mut `text_dim`. */
/* App surfaces (keep in sync with crates/theme/src/palettes.rs; lint_icons.py checks): --card is the ribbon surface `toolbar` the icons lie on, --bg the page (`panel` in light, `viewport_top` in dark), --line `border`, --fg `text`, --mut `text_dim`. */
:root{--bg:#fafbfc;--fg:#262a32;--card:#f6f7f9;--line:#d0d4dc;--mut:#6e7480;--sz:48px}
body.dark{--bg:#2e3440;--fg:#dee2e8;--card:#272b34;--line:#3f4552;--mut:#949ba8}
body{font:12px system-ui,sans-serif;margin:0;background:var(--bg);color:var(--fg)}
+2 -2
View File
@@ -110,7 +110,7 @@ pub(crate) fn breadcrumb(p: &egui::Painter, c: egui::Pos2, col: Color32) {
}
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
brand_mark_styled(p, c, 20.0, crate::theme::Tokens::get().logo, true);
brand_mark_styled(p, c, 20.0, solidcraft_theme::Tokens::get().logo, true);
}
/// The same original mark as a single quiet silhouette, without face seams or outlines.
@@ -127,7 +127,7 @@ fn brand_mark_styled(p: &egui::Painter, c: Pos2, height: f32, colours: [Color32;
};
// The faces are separated by gaps that show the application bar, not by white lines.
let [light, mid, dark] = colours;
let edge_col = crate::theme::Tokens::get().app_bar;
let edge_col = solidcraft_theme::Tokens::get().app_bar;
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(mid, &[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)]);
+2 -2
View File
@@ -1,7 +1,7 @@
//! Constraint, assembly snap, hole placement and dimension marker artwork.
use crate::dialogs_assembly::SnapKind;
use crate::theme::Tokens;
use egui::{Color32, Pos2, Stroke, vec2};
use solidcraft_theme::Tokens;
pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: SnapKind, col: Color32, tag: Option<&str>) {
// A dark halo under every stroke keeps the glyph readable on light and dark faces alike.
@@ -49,7 +49,7 @@ pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: Sn
// What it snaps to (hovered), or which component it is (picked): a small label.
if let Some(tag) = tag {
let t = Tokens::get();
let font = egui::FontId::proportional(crate::theme::Tokens::get().font_caption);
let font = egui::FontId::proportional(solidcraft_theme::Tokens::get().font_caption);
let g = painter.layout_no_wrap(tag.to_string(), font, t.text);
let at = c + vec2(12.0, 10.0);
let r = egui::Rect::from_min_size(at, g.size()).expand2(vec2(5.0, 2.0));
+2 -2
View File
@@ -1,8 +1,8 @@
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
use crate::theme::Tokens;
use egui::{Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
use solidcore_record::Vec3;
use solidcraft_engine::geom::Ring;
use solidcraft_theme::Tokens;
pub(crate) struct Facet {
pub(crate) direction: Vec3,
pub(crate) points: Ring<Pos2>,
@@ -196,7 +196,7 @@ pub(crate) fn axis_tip_scaled(painter: &egui::Painter, c: Pos2, name: &str, d: &
// On a hovered (white) disc the letter takes the axis colour.
let letter = if hovered { col } else { t.on_accent };
let label = name.to_uppercase();
let galley = painter.layout_no_wrap(label, crate::fonts::bold(crate::theme::Tokens::get().font_compass * scale), letter);
let galley = painter.layout_no_wrap(label, crate::fonts::bold(solidcraft_theme::Tokens::get().font_compass * scale), letter);
let origin = at - galley.size() / 2.0;
let ppp = painter.ctx().pixels_per_point().max(0.5);
let origin = pos2((origin.x * ppp).round() / ppp, (origin.y * ppp).round() / ppp);
+3 -3
View File
@@ -2,16 +2,16 @@
use egui::{Color32, FontId, Pos2, Shape, Stroke, vec2};
/// The agent pointer's colour (a theme token, so a colour vision mode can reach it).
pub(crate) fn agent() -> Color32 {
crate::theme::Tokens::get().icons.agent
solidcraft_theme::Tokens::get().icons.agent
}
pub(crate) fn paint(painter: &egui::Painter, p: Pos2) {
let ic = crate::theme::Tokens::get().icons;
let ic = solidcraft_theme::Tokens::get().icons;
// An arrow pointer, tip at `p`.
let pts: Vec<Pos2> =
[(0.0, 0.0), (0.0, 17.0), (4.5, 13.0), (7.5, 20.0), (10.5, 18.8), (7.5, 12.0), (13.0, 12.0)].iter().map(|(x, y)| p + vec2(*x, *y)).collect();
painter.add(Shape::convex_polygon(pts.clone(), ic.agent, Stroke::NONE));
painter.add(Shape::closed_line(pts, Stroke::new(1.5, ic.highlight)));
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(crate::theme::Tokens::get().font_small), ic.highlight);
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(solidcraft_theme::Tokens::get().font_small), ic.highlight);
let r = egui::Rect::from_min_size(p + vec2(15.0, 18.0), label.size() + vec2(10.0, 4.0));
painter.rect_filled(r, 4.0, ic.agent);
painter.galley(r.min + vec2(5.0, 2.0), label, ic.highlight);
@@ -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
@@ -4527,7 +4527,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"));
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "solidcraft-theme"
description = "The token contract UI components draw with: SolidCraft's light and dark tokens, colour vision, tab accents and the theme state a host sets"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
egui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
solidcraft-color = { workspace = true }
+25
View File
@@ -0,0 +1,25 @@
# solidcraft-theme
The token contract every UI component draws with. A component reads its colours and sizes from
`Tokens::get()` and its tab colours from `TabAccent::current(ctx)`; with nothing set, it draws with
SolidCraft's built-in dark tokens. The crate holds no data files and knows no theme-file format, so a
component vendored into another app draws with that app's tokens and needs no theme files, built-in
themes or icon artwork.
SolidCraft's theme system (theme files, their options, the built-in themes and the per-tab surface
fades) stays in the app: it resolves `Tokens` and hands them to this crate.
## API
| Item | What it is |
|---|---|
| `Tokens` | Every colour and size the UI draws with; `Tokens::light()` and `Tokens::dark()` are SolidCraft's values, written in code |
| `Tokens::get()`, `Tokens::view()` | The tokens to draw with now (the window's, and those drawn straight onto the 3D view) |
| `set_dark`, `set_vision`, `set_viewport_dark`, `set_viewport_background`, `set_logo_accent` | The thread's theme state, which the host sets every frame |
| `set_custom`, `custom`, `scoped_tokens`, `scoped_panel_tokens` | A host's own palette in place of the built-in one, for the thread or for a scope |
| `ColorVision` | Colour vision modes: a second set of values for the colours that carry a meaning |
| `TabAccent`, `TAB_NAMES` | A tab's selection colours, published per egui context |
| `set_published`, `PublishedTab`, `Environment`, `Surfaces`, `Fade` | A frame's per-tab palettes, as a host's theme system resolves and fades them |
| `apply(ctx)` | egui's visuals and spacing from the tokens |
| `contrast_ratio`, `composite`, `panel_surface_fill`, ... | WCAG contrast checks on token pairs |
| `icon_strokes` | The primary strokes the tokens share with SolidCraft's icon artwork |
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,21 @@
//! Original light and dark palette values. Compiled by the UI theme boundary.
use crate::theme::{AnalysisColors, IconColors, Tokens};
//! Original light and dark palette values.
use crate::{IconColors, Tokens};
use egui::Color32;
use solidcraft_color::AnalysisColors;
/// The primary (`active-3`) strokes, light then dark, that the tokens share with SolidCraft's icon artwork
/// (`assets/icons/ui/design/palette.toml`, including its colour vision variants). ui-egui's icon tests hold them equal.
pub mod icon_strokes {
pub const SKETCH_PRIMARY: [[u8; 3]; 2] = [[5, 127, 69], [18, 141, 69]];
pub const CONSTRAINT_PRIMARY: [[u8; 3]; 2] = [[202, 71, 61], [193, 99, 88]];
pub const CONSTRUCTION_PRIMARY: [[u8; 3]; 2] = [[156, 86, 0], [167, 93, 0]];
pub const SKETCH_RED_GREEN: [[u8; 3]; 2] = [[29, 125, 82], [29, 127, 137]];
pub const CONSTRUCTION_RED_GREEN: [[u8; 3]; 2] = [[185, 115, 0], [167, 93, 0]];
pub const CONSTRAINT_RED_GREEN: [[u8; 3]; 2] = [[175, 64, 56], [193, 99, 88]];
pub const CONSTRUCTION_BLUE_YELLOW: [[u8; 3]; 2] = [[185, 115, 0], [195, 125, 0]];
pub const CONSTRAINT_BLUE_YELLOW: [[u8; 3]; 2] = [[175, 64, 56], [133, 85, 74]];
}
use icon_strokes as strokes;
const fn icon_primary(rgb: [u8; 3]) -> Color32 {
Color32::from_rgb(rgb[0], rgb[1], rgb[2])
@@ -17,8 +32,7 @@ impl Tokens {
nav_tip_key_border: Color32::from_rgba_unmultiplied_const(128, 140, 160, 153),
nav_field: Color32::from_rgb(255, 255, 255),
borders: crate::theme::Borders::LEGACY,
icon_palette: None,
borders: crate::Borders::LEGACY,
file_dim: Color32::from_rgb(58, 70, 86),
file_recovery_well: Color32::from_rgb(247, 226, 184),
file_action_height: 32.0,
@@ -119,7 +133,7 @@ impl Tokens {
text_dim: Color32::from_rgb(110, 116, 128),
accent: Color32::from_rgb(38, 120, 218),
accent_soft: Color32::from_rgb(214, 230, 250),
sketch_accent: icon_primary(crate::icon_svg::SKETCH_PRIMARY[0]),
sketch_accent: icon_primary(strokes::SKETCH_PRIMARY[0]),
// Icon palette active-3 ramps; these accents have no compiled constants yet.
surface_accent: Color32::from_rgb(164, 80, 0),
mesh_accent: Color32::from_rgb(130, 103, 0),
@@ -172,7 +186,7 @@ impl Tokens {
axis_y: Color32::from_rgb(60, 170, 60),
axis_z: Color32::from_rgb(60, 100, 230),
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
construction_geom: icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[0]),
construction_geom: icon_primary(strokes::CONSTRUCTION_PRIMARY[0]),
body: Color32::from_rgb(176, 186, 198),
body_edge: Color32::from_rgb(40, 44, 52),
manipulator: Color32::from_rgb(40, 110, 220),
@@ -215,7 +229,7 @@ impl Tokens {
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
glyph_bg: Color32::WHITE,
glyph_edge: Color32::from_rgb(190, 110, 100),
glyph_text: icon_primary(crate::icon_svg::CONSTRAINT_PRIMARY[0]),
glyph_text: icon_primary(strokes::CONSTRAINT_PRIMARY[0]),
overlay: Color32::from_rgba_unmultiplied_const(255, 255, 255, 235),
rubber_band: Color32::from_rgb(30, 120, 230),
app_bar_text: Color32::from_rgb(230, 234, 242),
@@ -339,7 +353,7 @@ impl Tokens {
t.text_dim = Color32::from_rgb(148, 155, 168);
t.accent = Color32::from_rgb(58, 136, 238);
t.accent_soft = Color32::from_rgb(42, 70, 110);
t.sketch_accent = icon_primary(crate::icon_svg::SKETCH_PRIMARY[1]);
t.sketch_accent = icon_primary(strokes::SKETCH_PRIMARY[1]);
t.surface_accent = Color32::from_rgb(149, 104, 45);
t.mesh_accent = Color32::from_rgb(133, 113, 0);
t.form_accent = Color32::from_rgb(123, 99, 166);
@@ -399,7 +413,7 @@ impl Tokens {
t.pref_ok = Color32::from_rgb(127, 209, 174);
t.pref_ok_fill = Color32::from_rgb(31, 58, 49);
t.pref_disabled = Color32::from_rgb(138, 148, 163);
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]);
t.construction_geom = icon_primary(strokes::CONSTRUCTION_PRIMARY[1]);
t.error = Color32::from_rgb(240, 100, 96);
t.sketch_conflict = t.error;
t.warning = Color32::from_rgb(236, 170, 70);
@@ -430,7 +444,7 @@ impl Tokens {
t.chip = Color32::from_rgb(59, 101, 168);
t.glyph_bg = Color32::from_rgb(44, 50, 60);
t.glyph_edge = Color32::from_rgb(200, 130, 120);
t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_PRIMARY[1]);
t.glyph_text = icon_primary(strokes::CONSTRAINT_PRIMARY[1]);
t.overlay = Color32::from_rgba_unmultiplied_const(34, 38, 46, 240);
t.rubber_band = Color32::from_rgb(92, 160, 255);
t.timeline_item = Color32::from_rgb(52, 58, 70);
@@ -473,8 +487,8 @@ impl Tokens {
if !dark {
t.form_success = Color32::from_rgb(39, 88, 78);
}
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_RED_GREEN[dark as usize]);
t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_RED_GREEN[dark as usize]);
t.construction_geom = icon_primary(strokes::CONSTRUCTION_RED_GREEN[dark as usize]);
t.glyph_text = icon_primary(strokes::CONSTRAINT_RED_GREEN[dark as usize]);
// A deeper red keeps conflicting sketch geometry (#7244) apart from the orange
// construction and centerline lines and the crimson lost links for a red-green loss.
t.sketch_conflict = Color32::from_rgb(195, 35, 30);
@@ -484,14 +498,14 @@ impl Tokens {
if dark {
t.bend_up = Color32::from_rgb(110, 165, 255);
t.bend_down = Color32::from_rgb(240, 180, 70);
t.sketch_accent = icon_primary(crate::icon_svg::SKETCH_RED_GREEN[1]);
t.sketch_accent = icon_primary(strokes::SKETCH_RED_GREEN[1]);
} else {
t.sketch_projected = Color32::from_rgb(205, 120, 215);
t.sketch_locked = Color32::from_rgb(60, 150, 120);
t.timeline_item_error = Color32::from_rgb(232, 196, 170);
t.bend_up = Color32::from_rgb(30, 100, 210);
t.bend_down = Color32::from_rgb(200, 120, 0);
t.sketch_accent = icon_primary(crate::icon_svg::SKETCH_RED_GREEN[0]);
t.sketch_accent = icon_primary(strokes::SKETCH_RED_GREEN[0]);
}
t
}
@@ -506,8 +520,8 @@ impl Tokens {
// Green and blue status lights merge for tritan vision; use rose for working.
t.agent_busy = Color32::from_rgb(232, 150, 190);
t.preview_sketch = Color32::from_rgb(125, 180, 105);
t.glyph_text = icon_primary(crate::icon_svg::CONSTRAINT_BLUE_YELLOW[dark as usize]);
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_BLUE_YELLOW[dark as usize]);
t.glyph_text = icon_primary(strokes::CONSTRAINT_BLUE_YELLOW[dark as usize]);
t.construction_geom = icon_primary(strokes::CONSTRUCTION_BLUE_YELLOW[dark as usize]);
// Conflicting sketch geometry (#7244) apart from the lost links and the construction and
// centerline lines for a blue-yellow loss: darker on the light view, lighter on the dark.
t.sketch_conflict = if dark { Color32::from_rgb(255, 170, 160) } else { Color32::from_rgb(165, 15, 30) };
+6 -2
View File
@@ -11,6 +11,12 @@ 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 }
# The token contract every component draws with; the theme system here resolves and sets it.
solidcraft-theme = { workspace = true }
egui = { workspace = true }
egui-wgpu = { workspace = true }
solidcraft-gpu = { workspace = true }
@@ -25,10 +31,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.
-16
View File
@@ -184,22 +184,6 @@ fn icon_data() -> Result<(), String> {
}
let _ = writeln!(out, "pub(crate) const ACCENT_NAMES: &[&str] = &{:?};", accent_names);
let _ = writeln!(out, "pub(crate) const ACCENTS: &[&[(usize, [u8; 3], [u8; 3])]] = &[{}];", accents.join(", "));
// UI tokens and SVGs share the same primary strokes, including accessibility overrides.
for (constant, value) in [
("SKETCH_PRIMARY", palette.get("accents").and_then(|v| v.get("sketch"))),
("CONSTRAINT_PRIMARY", palette.get("accents").and_then(|v| v.get("constraint"))),
("CONSTRUCTION_PRIMARY", palette.get("accents").and_then(|v| v.get("construction"))),
("SKETCH_RED_GREEN", palette.get("variants").and_then(|v| v.get("red-green")).and_then(|v| v.get("sketch"))),
("CONSTRUCTION_RED_GREEN", palette.get("variants").and_then(|v| v.get("red-green")).and_then(|v| v.get("construction"))),
("CONSTRAINT_RED_GREEN", palette.get("variants").and_then(|v| v.get("red-green")).and_then(|v| v.get("constraint"))),
("CONSTRUCTION_BLUE_YELLOW", palette.get("variants").and_then(|v| v.get("blue-yellow")).and_then(|v| v.get("construction"))),
("CONSTRAINT_BLUE_YELLOW", palette.get("variants").and_then(|v| v.get("blue-yellow")).and_then(|v| v.get("constraint"))),
] {
let role = value.and_then(|v| v.get("active-3"));
let l = hex(role.and_then(|v| v.get("light")), constant)?;
let d = hex(role.and_then(|v| v.get("dark")), constant)?;
let _ = writeln!(out, "pub(crate) const {constant}: [[u8; 3]; 2] = [{}, {}];", rgb(&l), rgb(&d));
}
let mut variants = Vec::new();
if let Some(groups) = palette.get("variants").and_then(toml::Value::as_table) {
for (vision, group) in groups {
+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_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::{left_inset, owns_keyboard_input, 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())]))?;
+17 -17
View File
@@ -8,8 +8,8 @@ use crate::ui_kit::{Container, Edge, Surface};
use egui::{Align, Align2, Color32, Id, Layout, Rect, RichText, Sense, Stroke, pos2, vec2};
use super::*;
use crate::theme::Tokens;
use crate::ui_kit;
use solidcraft_theme::Tokens;
pub const MIN_WIDTH: f32 = 280.0;
pub const MAX_WIDTH: f32 = 760.0;
@@ -189,7 +189,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
}
@@ -205,9 +205,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
@@ -216,7 +216,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();
@@ -811,7 +811,7 @@ fn dock_drag(app: &mut SolveApp, ui: &egui::Ui, drag: &egui::Response, left: boo
zone.center(),
Align2::CENTER_CENTER,
tr(if to_left { "ui.agent_chat_dock_left" } else { "ui.agent_chat_dock_right" }),
egui::FontId::proportional(crate::theme::Tokens::get().font_heading),
egui::FontId::proportional(solidcraft_theme::Tokens::get().font_heading),
t.text,
);
}
@@ -1216,7 +1216,7 @@ fn switcher(app: &mut SolveApp, ui: &mut egui::Ui) {
}
});
if let Some(a) = &app.agent_chat.auth_status {
ui.label(RichText::new(a).color(t.text_dim).size(crate::theme::Tokens::get().font_small));
ui.label(RichText::new(a).color(t.text_dim).size(solidcraft_theme::Tokens::get().font_small));
}
}
@@ -1594,7 +1594,7 @@ fn auth_box(app: &mut SolveApp, ui: &mut egui::Ui) {
ui_kit::text(ui, tr("ui.agent_chat_auth_none"), ui_kit::Text::Note("auth_none"));
}
for m in methods {
ui.add_space(crate::theme::Tokens::get().space_4);
ui.add_space(solidcraft_theme::Tokens::get().space_4);
let desc = m.description.trim();
ui_kit::text(
ui,
@@ -1624,7 +1624,7 @@ fn auth_box(app: &mut SolveApp, ui: &mut egui::Ui) {
}
}
}
ui.add_space(crate::theme::Tokens::get().space_4);
ui.add_space(solidcraft_theme::Tokens::get().space_4);
if ui_kit::button(ui, tr("ui.agent_chat_auth_retry"), ui_kit::Button::Normal).clicked() {
connect(app, ui.ctx());
}
@@ -1659,7 +1659,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());
@@ -1872,9 +1872,9 @@ fn conversation(app: &mut SolveApp, ui: &mut egui::Ui) {
Item::Changes { steps, doc, undone } => {
ui.horizontal(|ui| {
let text = trf("ui.agent_chat_changes", &[("n", &steps.to_string())]);
ui.label(RichText::new(text).color(t.text_dim).size(crate::theme::Tokens::get().font_caption));
ui.label(RichText::new(text).color(t.text_dim).size(solidcraft_theme::Tokens::get().font_caption));
if *undone {
ui.label(RichText::new(tr("ui.agent_chat_undone")).color(t.text_dim).size(crate::theme::Tokens::get().font_caption));
ui.label(RichText::new(tr("ui.agent_chat_undone")).color(t.text_dim).size(solidcraft_theme::Tokens::get().font_caption));
} else if Arc::ptr_eq(doc, &current_doc) {
let b =
ui_kit::button(ui, tr("ui.agent_chat_undo_changes"), ui_kit::Button::Normal).backing_on_hover_text(tr("ui.ai_undo_tip"));
@@ -1911,7 +1911,7 @@ fn conversation(app: &mut SolveApp, ui: &mut egui::Ui) {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
let tag = if stopped && !busy { "ui.agent_chat_queued_stopped" } else { "ui.agent_chat_queued" };
ui.label(RichText::new(tr(tag)).color(t.accent).semibold().size(crate::theme::Tokens::get().font_small));
ui.label(RichText::new(tr(tag)).color(t.accent).semibold().size(solidcraft_theme::Tokens::get().font_small));
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let x = ui_kit::button(ui, "×", ui_kit::Button::Small).backing_on_hover_text(tr("ui.agent_chat_remove"));
crate::scenario::publish_handle(&format!("agent_chat:pending_remove:{n}"), x.rect.center());
@@ -1945,7 +1945,7 @@ fn conversation(app: &mut SolveApp, ui: &mut egui::Ui) {
super::markdown_view::selectable(ui, egui::Label::new(RichText::new(&p.text).color(t.text)).wrap());
}
if p.image {
ui.label(RichText::new(tr("ui.agent_chat_view_attached")).color(t.text_dim).size(crate::theme::Tokens::get().font_small));
ui.label(RichText::new(tr("ui.agent_chat_view_attached")).color(t.text_dim).size(solidcraft_theme::Tokens::get().font_small));
}
context_chips(ui, &p.context, false);
files_line(ui, p.files.iter().map(|f| f.name.as_str()));
@@ -2160,7 +2160,7 @@ fn chip(ui: &mut egui::Ui, thumbs: &mut std::collections::HashMap<u64, egui::Tex
r.center(),
Align2::CENTER_CENTER,
f.mark(),
egui::FontId::proportional(crate::theme::Tokens::get().font_micro),
egui::FontId::proportional(solidcraft_theme::Tokens::get().font_micro),
color,
);
r.width()
@@ -2419,10 +2419,10 @@ mod tests {
#[test]
fn assistant_header_and_composer_keep_actions_inside_each_panel_width() {
for dark in [false, true] {
crate::theme::set_dark(dark);
solidcraft_theme::set_dark(dark);
for width in [280.0, 440.0, 760.0] {
let ctx = crate::fonts::test_context();
crate::theme::apply(&ctx);
solidcraft_theme::apply(&ctx);
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
app.agent_chat.detection = Some(discover::Detection::default());
app.agent_chat.picking = Some(false);
+36 -36
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"));
@@ -618,7 +618,7 @@ fn permissions_section(app: &mut SolveApp, ui: &mut egui::Ui) {
}
let names: std::collections::HashMap<_, _> = agents::entries(app).into_iter().map(|entry| (entry.id, entry.name)).collect();
for (agent, tools) in remembered {
let t = crate::theme::Tokens::get();
let t = solidcraft_theme::Tokens::get();
ui_kit::settings_block(ui, names.get(&agent).unwrap_or(&agent), t.space_24 * 6.0 + t.space_6, |ui| {
ui.horizontal(|ui| {
let reset = ui_kit::button(ui, tr("ui.ai_reset"), ui_kit::Button::Small);
@@ -635,7 +635,7 @@ fn permissions_section(app: &mut SolveApp, ui: &mut egui::Ui) {
}
fn panel_section(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = crate::theme::Tokens::get();
let t = solidcraft_theme::Tokens::get();
ui_kit::settings_section(ui, "panel", tr("ui.ai_panel"), |ui| {
let label_width = [tr("ui.agent_chat_side"), tr("ui.agent_chat_turn_limit")]
.iter()
@@ -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;
let speed_label_width = ui_kit::ContentWidth::label(ui, tr("ai.prefs.pointer_speed")) + solidcraft_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),
);
});
}
@@ -767,7 +767,7 @@ fn agents_section(app: &mut SolveApp, ui: &mut egui::Ui) {
let mut retimed: Option<(String, u64)> = None;
let mut reeffort: Option<(String, String)> = None;
let mut picked_model: Option<(String, ui_kit::Picked)> = None;
let t = crate::theme::Tokens::get();
let t = solidcraft_theme::Tokens::get();
ui_kit::settings_section_count(ui, "agents", tr("ui.ai_agents"), Some(&all.len().to_string()), |ui| {
if app.agent_chat.detecting.is_some() {
ui_kit::text(ui, tr("ui.agent_chat_detecting"), ui_kit::Text::Hint("detecting"));
@@ -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);
}
});
}
@@ -1496,7 +1496,7 @@ mod card_tests {
.job
.sections
.iter()
.all(|section| section.format.font_id.size == crate::theme::Tokens::get().font_page_heading);
.all(|section| section.format.font_id.size == solidcraft_theme::Tokens::get().font_page_heading);
}
});
}
@@ -1513,7 +1513,7 @@ mod card_tests {
for key in ["assistant_availability", "permissions", "agents", "instructions", "panel", "assistant_connecting", "agent_pointer"] {
let section = scenario::rect_of(&format!("prefs:section:{key}")).unwrap();
assert!((section.left() - body.left()).abs() <= 1.0 && (section.right() - body.right()).abs() <= 1.0);
assert!((section.height() - (crate::theme::Tokens::get().control_height + crate::theme::Tokens::get().space_2)).abs() <= 1.0);
assert!((section.height() - (solidcraft_theme::Tokens::get().control_height + solidcraft_theme::Tokens::get().space_2)).abs() <= 1.0);
if let Some(bottom) = previous {
assert!((section.top() - bottom - 38.0).abs() <= 1.0, "{theme}: {key} retains the page section rhythm");
}
@@ -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;
+13 -13
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;
@@ -3200,7 +3200,7 @@ fn the_agent_list_keeps_the_panels_side_margin() {
});
}
let at = |label: &str| texts.iter().find(|(text, r)| text == label && panel.contains(r.center())).map(|(_, r)| *r);
let margin = crate::theme::Tokens::get().space_12 - 0.5;
let margin = solidcraft_theme::Tokens::get().space_12 - 0.5;
let agent = at(tr("ui.agent_chat_pick")).expect("the Agent row");
assert!(agent.left() >= panel.left() + margin, "{layout:?}: Agent at {agent:?} in {panel:?}");
let refresh = at(tr("ui.agent_chat_refresh")).expect("Refresh");
@@ -4614,12 +4614,12 @@ 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();
rows = crate::ui_kit::assistant_hint_galley(ui, &hint, width - 2.0 * solidcraft_theme::Tokens::get().form_inset).rows.len();
});
output.textures_delta.clear();
assert_eq!(rows, 1, "{language} {key} at {width} wide: {hint}");
@@ -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;
@@ -4699,7 +4699,7 @@ fn a_prompt_at_the_top_of_the_conversation_is_as_tall_as_its_content() {
let button = crate::scenario::handle_at("agent_chat:copy:0").expect("the prompt's Copy message button");
let prompt = crate::scenario::rect_of("agent_chat:msg:0").expect("the prompt's message area");
let reply = crate::scenario::rect_of("agent_chat:msg:1").expect("the reply's message area");
let row = crate::theme::Tokens::get().control_height;
let row = solidcraft_theme::Tokens::get().control_height;
assert!(prompt.bottom() - button.y < row, "the bubble ends {} below its button", prompt.bottom() - button.y);
assert!(reply.top() - button.y < 2.0 * row, "the reply starts {} below the prompt's button", reply.top() - button.y);
}
+3 -3
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()
},
);
@@ -493,7 +493,7 @@ pub fn paint(app: &SolveApp, ctx: &egui::Context) {
crate::icons::pointer::paint(&painter, p);
if let Some((_, label)) = app.agent.job.as_ref().and_then(|job| job.stops.front()).filter(|(_, label)| !label.is_empty()) {
use crate::ui_kit::{Container, Surface};
let t = crate::theme::Tokens::get();
let t = solidcraft_theme::Tokens::get();
Container::new(ctx, Surface::Popup)
.area(egui::Id::unique("sc_agent_command"))
.order(egui::Order::Debug)
@@ -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,
+2 -2
View File
@@ -213,7 +213,7 @@ impl State {
self.retire();
return Scope { previous, alpha };
}
let colors = crate::theme::Tokens::get();
let colors = solidcraft_theme::Tokens::get();
let background = [colors.viewport_top, colors.viewport_bottom, colors.app_bar];
if self.last_background != Some(background) {
let failed = self.failure.is_some() || self.active.as_ref().is_some_and(|s| s.inner.lock().is_ok_and(|i| i.failure.is_some()));
@@ -433,7 +433,7 @@ pub fn viewport(painter: &Painter, rect: Rect, cb: ViewportCallback, background:
if let Ok(mut inner) = shared.inner.lock() {
inner.source_count = inner.source_count.saturating_add(1);
}
let outside = crate::theme::Tokens::get().app_bar;
let outside = solidcraft_theme::Tokens::get().app_bar;
painter.add(egui_wgpu::Callback::new_paint_callback(
rect,
Source {
+7 -6
View File
@@ -9,7 +9,7 @@ use serde_json::{Map, Value, json};
use solidcraft_engine::doc::metadata::{self, BOM_KEYS};
use crate::SolveApp;
use crate::theme::Tokens;
use solidcraft_theme::Tokens;
/// The window's state; the rows come from the engine.
#[derive(Default)]
@@ -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;
}
@@ -408,7 +409,7 @@ mod tests {
h.frames(2);
let (Some(desc), Some(rev)) = (h.widget("Description"), h.widget("Rev")) else { panic!("the grid's headers are drawn") };
// Before, every column but the last was as narrow as a button and the grid used a third of the window.
let card = f64::from(crate::theme::Tokens::get().form_card_width);
let card = f64::from(solidcraft_theme::Tokens::get().form_card_width);
assert!(rev[0] - desc[0] > card, "Description spreads out: it starts at {}, Rev at {}", desc[0], rev[0]);
}
+20 -14
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::theme::Tokens;
use crate::context_menu::Target;
use crate::ui_kit::Item;
use solidcraft_theme::Tokens;
use crate::SolveApp;
@@ -1384,11 +1385,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)
@@ -1456,7 +1457,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);
@@ -1468,11 +1469,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;
@@ -2438,7 +2439,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));
}
@@ -2462,7 +2463,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()),
@@ -2602,7 +2603,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),
@@ -2628,9 +2633,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()
},
@@ -2994,7 +2999,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),
+4 -4
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
@@ -395,12 +395,12 @@ fn marker_row(app: &SolveApp, ui: &mut egui::Ui, depth: usize) -> egui::Response
let (rect, response) = ui.allocate_exact_size(vec2(ui.available_width(), t.space_12), Sense::drag());
crate::semantic_content::record_capture(ui, &response);
let line = Rect::from_min_max(pos2(rect.left() + 2.0 + depth as f32 * INDENT, rect.top()), rect.max);
crate::ui_kit::history_marker(ui.painter(), line, true, crate::theme::TabAccent::for_tab(&t, &app.ui.tab));
crate::ui_kit::history_marker(ui.painter(), line, true, solidcraft_theme::TabAccent::for_tab(&t, &app.ui.tab));
crate::scenario::publish_handle("browser:rollback", rect.center());
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
@@ -412,7 +412,7 @@ fn drag_marker(app: &mut SolveApp, ui: &egui::Ui, resp: &egui::Response, slots:
let Some(y) = ui.ctx().pointer_latest_pos().map(|p| p.y) else { return };
let Some(&(slot_y, position)) = slots.iter().min_by(|a, b| (a.0 - y).abs().total_cmp(&(b.0 - y).abs())) else { return };
if resp.dragged() && position != marker {
let accent = crate::theme::TabAccent::current(ui.ctx()).color;
let accent = solidcraft_theme::TabAccent::current(ui.ctx()).color;
ui.painter().hline(resp.rect.left() + 4.0..=resp.rect.right() - 4.0, slot_y, Stroke::new(2.0, accent));
}
if resp.drag_stopped()
+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());
+1 -1
View File
@@ -16,7 +16,7 @@ use egui::{Color32, Mesh, Pos2, Rect, Sense, Stroke, Ui, vec2};
use crate::ui_kit;
use crate::theme::Tokens;
use solidcraft_theme::Tokens;
/// The part of a range a gesture moves, and that playing moves.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+20 -14
View File
@@ -10,9 +10,9 @@ use solidcraft_engine::geom::prelude::*;
use crate::SolveApp;
use crate::browser::panel_pos;
use crate::theme::Tokens;
use crate::ui_kit;
use crate::viewport::Proj;
use solidcraft_theme::Tokens;
pub(crate) fn panel(app: &mut SolveApp, ctx: &egui::Context) {
let Some(mut m) = app.tree.canvas_panel.clone() else { return };
@@ -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
+15 -15
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() {
@@ -309,17 +309,17 @@ fn browser_card_shrinks_and_scrolls_without_covering_history() {
#[test]
fn selected_row_paints_tab_soft_fill_and_accent_text_in_both_layouts() {
let previous = crate::theme::is_dark();
let previous = solidcraft_theme::is_dark();
for dark in [false, true] {
crate::theme::set_dark(dark);
solidcraft_theme::set_dark(dark);
for mode in [LayoutMode::Floating, LayoutMode::Docked] {
for tab in ["MODEL", "SKETCH", "SURFACE", "MESH", "FORM", "SHEET METAL", "RENDER"] {
let ctx = crate::fonts::test_context();
let mut app = SolveApp::new(Default::default(), Default::default());
app.ui.layout = mode;
app.ui.tab = tab.into();
crate::theme::TabAccent::publish(&app, &ctx);
let accent = crate::theme::TabAccent::for_tab(&Tokens::get(), tab);
app.publish_tab_accent(&ctx);
let accent = solidcraft_theme::TabAccent::for_tab(&Tokens::get(), tab);
let mut row_rect = Rect::NOTHING;
let out = crate::test_support::frame(&ctx, egui::RawInput::default(), |ui| {
let row = Row { selected: true, label: "Selected body", dim: true, color: Some(Tokens::get().error), ..Default::default() };
@@ -335,7 +335,7 @@ fn selected_row_paints_tab_soft_fill_and_accent_text_in_both_layouts() {
}
}
}
crate::theme::set_dark(previous);
solidcraft_theme::set_dark(previous);
}
#[test]
@@ -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));
}
@@ -667,9 +667,9 @@ fn a_long_body_name_stays_inside_the_card_and_ends_in_an_ellipsis() {
#[test]
fn collapsed_rail_shares_header_hover_inset_and_expands_from_its_whole_card() {
let previous = crate::theme::is_dark();
let previous = solidcraft_theme::is_dark();
for dark in [false, true] {
crate::theme::set_dark(dark);
solidcraft_theme::set_dark(dark);
for collapsed in [false, true] {
let ctx = crate::fonts::test_context();
ctx.global_style_mut(|style| style.animation_time = 0.0);
@@ -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| {
@@ -694,7 +694,7 @@ fn collapsed_rail_shares_header_hover_inset_and_expands_from_its_whole_card() {
draw(&mut app, vec![egui::Event::PointerMoved(at)]);
let output = draw(&mut app, vec![egui::Event::PointerMoved(at)]);
assert_eq!(output.platform_output.cursor_icon, egui::CursorIcon::PointingHand);
let accent = crate::theme::TabAccent::current(&ctx);
let accent = solidcraft_theme::TabAccent::current(&ctx);
let mut hover = None;
for shape in &output.shapes {
crate::test_support::each_shape(&shape.shape, &mut |shape| {
@@ -719,7 +719,7 @@ fn collapsed_rail_shares_header_hover_inset_and_expands_from_its_whole_card() {
}
}
}
crate::theme::set_dark(previous);
solidcraft_theme::set_dark(previous);
}
/// #6907: a mesh body's row has its own icon and tooltip, so it can be told from a solid one.
@@ -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"),
+15 -11
View File
@@ -16,8 +16,8 @@ use solidcraft_engine::geom::prelude::*;
use crate::SolveApp;
use crate::dialogs::{Dialog, Kind};
use crate::theme::Tokens;
use crate::viewport::{Proj, projected_polygon};
use solidcraft_theme::Tokens;
/// Where the dialog's geometry is: a base point and, for values that can be dragged, the
/// direction a growing value points.
@@ -955,7 +955,9 @@ 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(solidcraft_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 {
@@ -966,7 +968,7 @@ fn value_box_full(
st.store(ui.ctx(), id);
}
if let Some(note) = note {
ui.label(egui::RichText::new(note).color(t.text_dim).size(crate::theme::Tokens::get().font_caption));
ui.label(egui::RichText::new(note).color(t.text_dim).size(solidcraft_theme::Tokens::get().font_caption));
}
field = Some(r.rect);
});
@@ -1049,7 +1051,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 +1326,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),
@@ -1502,7 +1504,8 @@ fn measure_line(
painter.circle(s, 3.5, t.manipulator, Stroke::new(1.0, Color32::WHITE));
}
let mid = sa + (sb - sa) * 0.5;
let galley = painter.layout_no_wrap(crate::prefs::show_mm(app, d, 1), egui::FontId::proportional(crate::theme::Tokens::get().font_label), t.text);
let galley =
painter.layout_no_wrap(crate::prefs::show_mm(app, d, 1), egui::FontId::proportional(solidcraft_theme::Tokens::get().font_label), t.text);
let own = egui::Rect::from_min_size(mid + vec2(10.0, -14.0), galley.size() + vec2(10.0, 6.0));
let rect = moved_label(proj, own, labels.offset(0));
pick_numbers(painter, sa, sb, rect);
@@ -1588,7 +1591,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));
@@ -1601,7 +1604,7 @@ fn measure_components(
(total.center(), vec2(20.0, 20.0))
};
let text = format!("{}: {}", axis.to_uppercase(), crate::prefs::show_mm(app, distance, 1));
let galley = painter.layout_no_wrap(text, egui::FontId::proportional(crate::theme::Tokens::get().font_label), t.text);
let galley = painter.layout_no_wrap(text, egui::FontId::proportional(solidcraft_theme::Tokens::get().font_label), t.text);
let size = galley.size() + vec2(10.0, 6.0);
let bounds = proj.rect.shrink(4.0);
let clamp = |rect: egui::Rect| {
@@ -1709,7 +1712,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 +1904,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));
}
}
+7 -7
View File
@@ -1,4 +1,4 @@
//! Every colour that carries a meaning comes from `theme::Tokens` (#778): the engine's sketch and
//! Every colour that carries a meaning comes from `solidcraft_theme::Tokens` (#778): the engine's sketch and
//! axis tags, the surface analysis colours and every colour an icon draws. These tests hold that
//! the tokens equal what was drawn before, and that a new colour cannot skip them.
@@ -6,7 +6,7 @@ use egui::Color32;
use solidcraft_engine::render::Rgb;
use solidcraft_engine::view::colors;
use crate::theme::Tokens;
use solidcraft_theme::Tokens;
fn rgb(c: Rgb) -> Color32 {
Color32::from_rgb(c.0, c.1, c.2)
@@ -69,9 +69,9 @@ fn stroke_colours(shapes: &[egui::epaint::ClippedShape]) -> std::collections::BT
/// cube's triad, 3D sketch triads and joint origins draw them.
#[test]
fn every_axis_is_drawn_in_one_set_of_colours() {
use crate::theme::{ColorVision, set_dark, set_vision};
use egui::{Pos2, Rect, vec2};
use solidcore_record::Vec3;
use solidcraft_theme::{ColorVision, set_dark, set_vision};
let ctx = crate::fonts::test_context();
let rect = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));
let cam = solidcraft_engine::render::Camera::default();
@@ -139,7 +139,7 @@ fn analysis_colours_reach_the_shader_in_its_order() {
/// The most colour literals each UI file may hold, and why. None of them carries a meaning that a
/// colour vision mode must change. A file with more fails here: make the new colour a token in
/// `theme::Tokens` (and `assets/styles/ui/palettes.rs`) instead, or, if it carries no meaning,
/// `solidcraft_theme::Tokens` (and `crates/theme/src/palettes.rs`) instead, or, if it carries no meaning,
/// raise the file's count here and give the reason. Fewer is always fine. The icon sources may
/// hold none at all.
const ALLOWED_LITERALS: &[(&str, usize, &str)] = &[
@@ -231,7 +231,7 @@ fn colours_outside_the_theme_are_only_the_listed_neutral_ones() {
for dir in &dirs {
scan(dir, "", &mut found);
}
assert!(found.is_empty(), "colour literals outside theme::Tokens: {found:?}");
assert!(found.is_empty(), "colour literals outside solidcraft_theme::Tokens: {found:?}");
}
/// The analysis shader takes its colours from the uniform (and so from the tokens). Only the
@@ -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`");
}
@@ -310,7 +310,7 @@ fn the_logo_can_take_the_themes_accent() {
struct Reset;
impl Drop for Reset {
fn drop(&mut self) {
crate::theme::set_logo_accent(false);
solidcraft_theme::set_logo_accent(false);
}
}
let _reset = Reset;
+15 -16
View File
@@ -5,15 +5,15 @@
//! with their paper, Brettel on white and its own anchors.
//!
//! When a token edit breaks a mode here, adjust its measured variant. Shared icon accents live
//! in `assets/icons/ui/design/palette.toml`; `assets/styles/ui/palettes.rs` links those generated
//! values to UI tokens and owns the remaining UI-only colour-vision adjustments.
//! in `assets/icons/ui/design/palette.toml`; `crates/theme/src/palettes.rs` holds the primary
//! strokes the UI tokens share with them (`icon_strokes`, held equal by an icon test) and owns the remaining UI-only colour-vision adjustments.
use std::collections::BTreeSet;
use egui::Color32;
use serde_json::json;
use crate::theme::{ColorVision, Tokens};
use solidcraft_theme::{ColorVision, Tokens};
type Rgb8 = [u8; 3];
type V3 = [f64; 3];
@@ -320,7 +320,7 @@ fn groups(t: &Tokens) -> Vec<Group> {
&[("figure_ground", t.figure_ground), ("figure_face_top", t.figure_face_top)],
),
// Badge text and selected-card marks use these exact pairs; WCAG ratios are also
// guarded by theme::tests::preferences_foreground_pairs_hold_small_text_contrast.
// guarded by solidcraft_theme's preferences_foreground_pairs_hold_small_text_contrast.
group("Preferences warning badge", &[("pref_note", t.pref_note)], &[("pref_note_fill", t.pref_note_fill)]),
group("Preferences success badge", &[("pref_ok", t.pref_ok)], &[("pref_ok_fill", t.pref_ok_fill)]),
group("Preferences selected marks", &[("pref_on_accent", t.pref_on_accent)], &[("figure_accent", t.figure_accent)]),
@@ -397,9 +397,9 @@ fn groups(t: &Tokens) -> Vec<Group> {
group(
"curvature map",
&[
("analysis.curvature[0]", crate::theme::srgb(a.curvature[0])),
("analysis.curvature[1]", crate::theme::srgb(a.curvature[1])),
("analysis.curvature[2]", crate::theme::srgb(a.curvature[2])),
("analysis.curvature[0]", solidcraft_theme::srgb(a.curvature[0])),
("analysis.curvature[1]", solidcraft_theme::srgb(a.curvature[1])),
("analysis.curvature[2]", solidcraft_theme::srgb(a.curvature[2])),
],
&[],
),
@@ -417,7 +417,7 @@ fn groups(t: &Tokens) -> Vec<Group> {
];
// Tab fills and small-text inks have distinct jobs: compare each with its actual
// grounds, rather than with the same-hue fill/ink. WCAG text ratios are also guarded
// in theme::tests::tab_accents_hold_wcag_small_text_contrast_in_both_themes.
// in solidcraft_theme's tab_accents_hold_wcag_small_text_contrast_in_both_themes.
for (name, ink_name, ink, soft_name, soft) in [
("sketch tab text", "sketch_ink", t.sketch_ink, "sketch_soft", t.sketch_soft),
("surface tab text", "surface_ink", t.surface_ink, "surface_soft", t.surface_soft),
@@ -502,7 +502,6 @@ const NO_MEANING: &[(&str, &str)] = &[
("borders.control", "structural chrome: hierarchy uses width, pattern and placement, not hue"),
("borders.primary_inset", "structural chrome: hierarchy uses width, pattern and placement, not hue"),
("borders.bevel", "structural chrome: hierarchy uses width, pattern and placement, not hue"),
("icon_palette", "optional SVG palette; actual roles and theme contrast are guarded in icon and theme tests"),
("file_dim", "File view secondary text, WCAG guarded in theme tests"),
("form_primary_fill", "ground under a filled primary field; form_primary carries the meaning, contrast guarded in theme tests"),
("file_recovery_well", "decorative recovery icon ground"),
@@ -983,7 +982,7 @@ fn validation_badges_are_distinct_and_readable_in_every_mode() {
if distance < FLOOR {
failures.push(format!("{mode:?} dark={dark} validation fills: {distance:.2}"));
}
let contrast = crate::theme::contrast_ratio(t.form_success, t.form_success_fill);
let contrast = solidcraft_theme::contrast_ratio(t.form_success, t.form_success_fill);
if contrast < 4.5 {
failures.push(format!("{mode:?} dark={dark} success contrast: {contrast:.2}"));
}
@@ -998,8 +997,8 @@ fn validation_badges_are_distinct_and_readable_in_every_mode() {
/// two-colour themes (1-bit) tell them apart by shape alone.
#[test]
fn shipped_themes_draw_dimensions_in_their_own_readable_colour() {
use crate::theme::contrast_ratio;
use crate::theme_files::{builtin, is_two_colour, resolve};
use solidcraft_theme::contrast_ratio;
let themes = [("light", Tokens::light()), ("dark", Tokens::dark())]
.into_iter()
.map(|(id, t)| (id.to_string(), t))
@@ -1052,7 +1051,7 @@ fn onyx_keeps_gold_dimensions_distinct_from_grey_construction_lines() {
assert!(normal(rgb(dimension), rgb(construction)) >= FLOOR, "{foreground}: dimensions remain distinct");
}
for ground in [t.viewport_top, t.viewport_bottom] {
assert!(crate::theme::contrast_ratio(construction, ground) >= 3.0, "{foreground}: readable construction lines");
assert!(solidcraft_theme::contrast_ratio(construction, ground) >= 3.0, "{foreground}: readable construction lines");
}
}
}
@@ -1226,11 +1225,11 @@ fn the_sample_previews_the_mode_being_chosen() {
for (colour, y) in [(want.bend_up, 14), (want.bend_down, 30)] {
assert!((12..72).any(|x| near(images[3].get_pixel(x, y).0, colour)), "{base} {mode:?}: bend stroke {colour:?}");
}
pixel(4, 18, 18, crate::theme::srgb(want.analysis.draft[0]));
pixel(4, 40, 18, crate::theme::srgb(want.analysis.draft[1]));
pixel(4, 18, 18, solidcraft_theme::srgb(want.analysis.draft[0]));
pixel(4, 40, 18, solidcraft_theme::srgb(want.analysis.draft[1]));
pixel(4, 2, 2, want.figure_ground);
let negative = crate::theme::srgb(want.analysis.draft[2]);
let hatch = crate::theme::srgb(want.analysis.draft[2].map(|c| c * 0.6));
let negative = solidcraft_theme::srgb(want.analysis.draft[2]);
let hatch = solidcraft_theme::srgb(want.analysis.draft[2].map(|c| c * 0.6));
let draft = &images[4];
let band: Vec<_> = (54..74).flat_map(|x| (24..34).map(move |y| draft.get_pixel(x, y).0)).collect();
assert!(band.iter().any(|p| near(*p, negative)), "{base} {mode:?}: Draft retains its negative analysis colour");
+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())
}
+18 -17
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 {
@@ -111,24 +111,25 @@ pub(super) fn show(ui: &mut Ui, app: &mut SolveApp, error: &mut Option<String>,
RuleEffect::AllowParameterValues { parameter, expressions } => format!("Restricts {parameter}: {}", expressions.join(", ")),
};
let actions_width = ["Edit", "↑", "↓", "Delete"].iter().map(|label| ui_kit::ContentWidth::button(ui, label)).sum::<f32>()
+ crate::theme::Tokens::get().space_4 * 3.0;
+ solidcraft_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);
}
});
+16 -16
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);
@@ -53,14 +53,14 @@ pub(super) fn configurations_rows(
.enumerate()
.map(|(index, label)| {
let min_width = if index == 0 {
name_width + gap + status_width + crate::theme::Tokens::get().space_24
name_width + gap + status_width + solidcraft_theme::Tokens::get().space_24
} else {
ui_kit::row_height() * 2.0 + crate::theme::Tokens::get().space_24
ui_kit::row_height() * 2.0 + solidcraft_theme::Tokens::get().space_24
};
if index == labels.len() - 1 {
ui_kit::Column::new(label, 0.0)
.icon_only()
.min_content_width(crate::theme::Tokens::get().control_height + crate::theme::Tokens::get().space_12)
.min_content_width(solidcraft_theme::Tokens::get().control_height + solidcraft_theme::Tokens::get().space_12)
} else {
let weight = if index == 0 {
1.6
@@ -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,11 +361,11 @@ 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.
let list_height = crate::theme::Tokens::get().space_24 * 12.0;
let list_height = solidcraft_theme::Tokens::get().space_24 * 12.0;
egui::ScrollArea::vertical().max_height(list_height).min_scrolled_height(list_height).show(ui, |ui| {
for (label, p) in &matching {
if ui_kit::button(ui, label.as_str(), ui_kit::Button::Normal).clicked() {
@@ -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);
+1 -1
View File
@@ -2446,7 +2446,7 @@ mod tests {
*counts.entry(img.get_pixel(x, 0).0).or_insert(0) += 1;
}
let top = counts.into_iter().max_by_key(|(_, n)| *n).map(|(c, _)| c).unwrap();
let want = if dark { crate::theme::Tokens::dark() } else { crate::theme::Tokens::light() }.viewport_top;
let want = if dark { solidcraft_theme::Tokens::dark() } else { solidcraft_theme::Tokens::light() }.viewport_top;
let near = top.iter().zip([want.r(), want.g(), want.b()]).all(|(a, b)| a.abs_diff(b) <= 2);
assert!(near, "{theme}: the render's top row is {top:?}, the view's background {want:?}");
}
+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);
+27 -23
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"));
ui.spacing_mut().item_spacing.y = solidcraft_theme::Tokens::get().space_10;
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,44 +97,48 @@ 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(
ui,
"reports",
crate::theme::Tokens::get().control_height * 10.0 + crate::theme::Tokens::get().space_10 * 4.0,
solidcraft_theme::Tokens::get().control_height * 10.0 + solidcraft_theme::Tokens::get().space_10 * 4.0,
|ui| {
let last = app.crashes.reports.len().saturating_sub(1);
for (index, r) in app.crashes.reports.iter().enumerate() {
let title = report_heading(r);
ui.push_id(&r.id, |ui| {
ui.spacing_mut().item_spacing.y = crate::theme::Tokens::get().space_6;
ui.add_space(crate::theme::Tokens::get().space_10);
ui.spacing_mut().item_spacing.y = solidcraft_theme::Tokens::get().space_6;
ui.add_space(solidcraft_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);
ui.add_space(solidcraft_theme::Tokens::get().space_6);
if index != last {
ui_kit::report_divider(ui, &format!("end:{}", r.id));
}
@@ -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);
}
@@ -323,7 +327,7 @@ mod tests {
let badge = crate::scenario::rect_of("prefs:crash_count").unwrap();
assert!((badge.height() - 18.0).abs() < 0.5);
assert!(crate::scenario::rect_of("prefs:page_body").unwrap().contains_rect(badge));
let t = crate::theme::Tokens::get();
let t = solidcraft_theme::Tokens::get();
let fill = if count == 0 { t.dialog_footer } else { t.pref_note_fill };
assert!(
h.shapes.iter().any(|shape| matches!(&shape.shape, egui::Shape::Rect(rect) if rect.rect == badge && rect.fill == fill)),
+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),
);
}
+17 -15
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)])
}
}
@@ -377,7 +379,7 @@ fn centre_and_axes(v: &Value) -> Option<(Vec3, Vec<Vec3>, f64)> {
pub fn overlay(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
let Some((centre, axes, reach)) = app.menu.props.as_ref().and_then(|(_, v)| centre_and_axes(v)) else { return };
let Some(at) = proj.to_screen(centre) else { return };
let tk = crate::theme::Tokens::view();
let tk = solidcraft_theme::Tokens::view();
for (n, (axis, colour)) in axes.iter().zip(tk.axes()).enumerate() {
let Some(tip) = proj.to_screen(centre + *axis * reach) else { continue };
painter.line_segment([at, tip], Stroke::new(2.0, colour));
@@ -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);
}
+1 -1
View File
@@ -18,8 +18,8 @@ use egui::{Rect, pos2};
use serde::{Deserialize, Serialize};
use crate::SolveApp;
use crate::theme::Tokens;
use crate::ui_kit::{Container, Surface};
use solidcraft_theme::Tokens;
/// Where command dialogs open (Preferences › Interface › Dialogs).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+33 -29
View File
@@ -21,9 +21,9 @@ use crate::selection::{
self, AXES, AXIS_PICKS, Accept, BODIES, CURVES, EDGES, FACES, PLANAR_FACES, PLANES, PROFILES, SelInput, SelectionExt as _, point_value as pt,
polyline_mid,
};
use crate::theme::Tokens;
use crate::tool_modifiers::{Audit, Scope, ToolModifiers};
use crate::viewport::Hit;
use solidcraft_theme::Tokens;
#[path = "dialogs_dense.rs"]
mod dense;
@@ -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) {
@@ -2743,7 +2743,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);
@@ -2915,25 +2916,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"),
@@ -3033,18 +3036,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 };
@@ -3074,7 +3077,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))
};
@@ -3099,7 +3102,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() {
@@ -3231,7 +3234,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);
@@ -3263,13 +3266,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);
@@ -3359,7 +3362,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");
@@ -3397,7 +3400,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
@@ -3764,7 +3767,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)
{
@@ -3832,7 +3835,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(&d.kind);
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 { .. });
@@ -3932,7 +3936,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() }
};
@@ -5748,10 +5752,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 } => {
@@ -8206,7 +8210,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;
+59 -54
View File
@@ -26,7 +26,7 @@ use crate::preview::{Built, Colors};
use crate::selection::{
BODIES, EDGES, FACES, ORIGIN, POINTS, SelInput, SelectionExt as _, VERTICES, items as sel_items, point_value as pt, polyline_mid,
};
use crate::theme::Tokens;
use solidcraft_theme::Tokens;
use crate::viewport::Proj;
@@ -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;
@@ -1015,7 +1015,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Asm, inputs: &mut [Se
ui.vertical(|ui| {
ui.label(
RichText::new(format!("{} … {}{}", fmt(lo), fmt(hi), if limited { " (limits)" } else { " (no limits)" }))
.size(crate::theme::Tokens::get().font_small)
.size(solidcraft_theme::Tokens::get().font_small)
.color(if limited { t.text } else { t.text_dim }),
);
});
@@ -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(solidcraft_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(solidcraft_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(solidcraft_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();
}
@@ -1952,7 +1957,7 @@ fn joint_group_button(
at: egui::Pos2,
count: usize,
open: bool,
accent: crate::theme::TabAccent,
accent: solidcraft_theme::TabAccent,
run: JointRun,
) -> (f32, bool) {
let chip = crate::ui_kit::HistoryGroupChip {
@@ -2003,7 +2008,7 @@ pub(crate) fn joint_chips(app: &mut SolveApp, ui: &mut egui::Ui, p: &egui::Paint
}
if grouped(app, joints.len()) {
let open = joints_open(app, joints.len(), set);
let accent = crate::theme::TabAccent::for_tab(&t, &app.ui.tab);
let accent = solidcraft_theme::TabAccent::for_tab(&t, &app.ui.tab);
let (width, clicked) = joint_group_button(ui, p, egui::pos2(x, r.top() + 6.0), joints.len(), open, accent, set);
if clicked {
toggle_joints(app, set);
@@ -2078,7 +2083,7 @@ pub(crate) fn timeline_joint_rows(app: &mut SolveApp, ui: &mut egui::Ui, ids: Op
let (rect, _) = ui.allocate_exact_size(vec2(t.form_section_icon_size, t.form_section_icon_size), egui::Sense::hover());
let fold = crate::ui_kit::disclosure_button_at(ui, rect, shown, &header);
let response = ui.add(
egui::Label::new(egui::RichText::new(&header).size(crate::theme::Tokens::get().font_micro).semibold().color(t.text_dim))
egui::Label::new(egui::RichText::new(&header).size(solidcraft_theme::Tokens::get().font_micro).semibold().color(t.text_dim))
.sense(egui::Sense::click()),
);
crate::scenario::publish_handle("timeline:joints", response.rect.center());
@@ -2092,7 +2097,7 @@ pub(crate) fn timeline_joint_rows(app: &mut SolveApp, ui: &mut egui::Ui, ids: Op
return;
}
} else {
ui.label(egui::RichText::new("JOINTS").size(crate::theme::Tokens::get().font_micro).semibold().color(t.text_dim));
ui.label(egui::RichText::new("JOINTS").size(solidcraft_theme::Tokens::get().font_micro).semibold().color(t.text_dim));
}
}
let mut open = None;
@@ -2119,7 +2124,7 @@ pub(crate) fn timeline_joint_rows(app: &mut SolveApp, ui: &mut egui::Ui, ids: Op
egui::pos2(icon.right() + 7.0, rect.center().y),
egui::Align2::LEFT_CENTER,
&name,
egui::FontId::proportional(crate::theme::Tokens::get().font_body),
egui::FontId::proportional(solidcraft_theme::Tokens::get().font_body),
if suppressed { t.text_dim } else { t.text },
);
let response = response.backing_on_hover_text(format!("{name} ({kind:?} joint)\nDouble-click to edit · right-click for more"));
+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");
+6 -6
View File
@@ -16,7 +16,7 @@ use crate::SolveApp;
use crate::dialogs::{combo, field, form_row};
use crate::dialogs_assembly::Asm;
use crate::selection::{EDGES, FACES, PROFILES, SelInput, SelectionExt as _, VERTICES};
use crate::theme::Tokens;
use solidcraft_theme::Tokens;
/// The follower types, as `joint.cam` takes them, and their labels.
const FOLLOWERS: [&str; 3] = ["knife", "roller", "flat"];
@@ -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"), "尖顶");
}
+5 -5
View File
@@ -16,7 +16,7 @@ use crate::SolveApp;
use crate::dialogs::{field, form_row};
use crate::dialogs_assembly::Asm;
use crate::selection::{EDGES, FACES, SelInput};
use crate::theme::Tokens;
use solidcraft_theme::Tokens;
/// What a round-face input takes: a face, or an edge of one.
const ROUND: u16 = FACES | EDGES;
@@ -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"), "円筒面ではありません");
}
+19 -18
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,15 +1159,15 @@ 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| {
let size = crate::theme::Tokens::get().form_close_size;
form_row(ui, solidcraft_ui_text::tr_label("Distance"), |ui| {
let size = solidcraft_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| {
ui.set_max_width((ui.available_width() - size).max(0.0));
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.

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