mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff90f2a7f0 | |||
| 8c4fd59e4c | |||
| bb38419b11 | |||
| 0e23fdd570 | |||
| bcd63adb98 | |||
| a9d6c7d810 | |||
| ec36c0b04d | |||
| e2664ea368 | |||
| fe45f81f56 | |||
| 6e8165674e | |||
| e3caced67b | |||
| d996441241 | |||
| 8a40998c2a | |||
| 924585be0a | |||
| b2c0fd71d0 | |||
| 2605261665 | |||
| 397bbf38c7 | |||
| 4db733ec68 |
@@ -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
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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": "删除"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 }
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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 }
|
||||
@@ -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) };
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())]))?;
|
||||
|
||||
@@ -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, ¤t_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);
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (編集中)"]);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"), "尖顶");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"), "円筒面ではありません");
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user