Rename displayed product to SolidCraft and add header bug reporting
This commit is contained in:
1 parent
1d3048062d
commit
af536a7907
85 files changed
+442
-242
No files matched your search
@@ -11,14 +11,14 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const COMMIT: &str = env!("SOLVECRAFT_COMMIT");
|
||||
pub const BUILD_DATE: &str = env!("SOLVECRAFT_BUILD_DATE");
|
||||
/// The user and developer guide (docs/book, published by .github/workflows/pages.yml).
|
||||
pub const DOCS_URL: &str = "https://bherbruck.github.io/solvecraft/";
|
||||
pub const DOCS_URL: &str = "https://bherbruck.github.io/solidcraft/";
|
||||
const ATTRIBUTION: &str = include_str!("../../../ATTRIBUTION.md");
|
||||
use solvecraft_engine::licences::{LICENSE_APACHE, LICENSE_MIT, NOTICE, THIRD_PARTY};
|
||||
/// Fonts: what this build carries.
|
||||
pub const FONTS: &str = "The interface uses egui's built-in fonts (Ubuntu Light and Hack, under the Ubuntu Font Licence and \
|
||||
the MIT/Bitstream Vera licence; Noto Emoji and the emoji icon font, under the OFL and MIT). Sketch text uses Liberation Sans \
|
||||
(SIL OFL 1.1). The Crafting Apps' optional font pack, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans \
|
||||
CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolveCraft builds.";
|
||||
CJK SC, Noto Sans Arabic; SIL OFL 1.1), is not bundled in SolidCraft builds.";
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HelpState {
|
||||
@@ -32,7 +32,7 @@ pub struct HelpState {
|
||||
pub fn diagnostics(app: &SolveApp) -> String {
|
||||
let renderer = if app.viewport.gpu.is_some() { "GPU" } else { "CPU" };
|
||||
format!(
|
||||
"SolveCraft {VERSION} ({COMMIT}, built {BUILD_DATE})\nOS: {} {}\nRenderer: {renderer}{}\nWindow: {}\nDesign: {} ({} features, {} bodies)",
|
||||
"SolidCraft {VERSION} ({COMMIT}, built {BUILD_DATE})\nOS: {} {}\nRenderer: {renderer}{}\nWindow: {}\nDesign: {} ({} features, {} bodies)",
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH,
|
||||
app.help.gpu.as_ref().map(|g| format!(", {g}")).unwrap_or_default(),
|
||||
@@ -71,7 +71,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
|
||||
crate::context_menu::menu_frame(ui, |ui| {
|
||||
for (id, label) in [
|
||||
("docs", "Documentation"),
|
||||
("about", "About SolveCraft"),
|
||||
("about", "About SolidCraft"),
|
||||
("shortcuts", "Keyboard Shortcuts"),
|
||||
("report", "Report an Issue…"),
|
||||
] {
|
||||
@@ -100,12 +100,12 @@ fn about(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
let t = Tokens::get();
|
||||
let mut open = true;
|
||||
crate::frame::window(ctx, "About SolveCraft", crate::frame::Width::Normal)
|
||||
crate::frame::window(ctx, "About SolidCraft", crate::frame::Width::Normal)
|
||||
.id(egui::Id::new("sc_about"))
|
||||
.open(&mut open)
|
||||
.default_size(vec2(440.0, 560.0))
|
||||
.show(ctx, |ui| {
|
||||
ui.label(RichText::new("SolveCraft").size(22.0).strong());
|
||||
ui.label(RichText::new("SolidCraft").size(22.0).strong());
|
||||
ui.label(format!("Version {VERSION} · commit {COMMIT} · built {BUILD_DATE}"));
|
||||
ui.label(RichText::new("Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.").color(t.text_dim));
|
||||
ui.add_space(6.0);
|
||||
|
||||
@@ -343,7 +343,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(36, 24))).show(ui, |ui| {
|
||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("SolveCraft").size(26.0).strong().color(t.text));
|
||||
ui.label(RichText::new("SolidCraft").size(26.0).strong().color(t.text));
|
||||
ui.add_space(12.0);
|
||||
ui.label(RichText::new("Start").size(16.0).color(t.text_dim));
|
||||
});
|
||||
|
||||
@@ -192,6 +192,17 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
pen.poly(&[(4.0, 7.5), (12.0, 12.0), (12.0, 21.0), (4.0, 16.5)], a, ink);
|
||||
pen.poly(&[(12.0, 12.0), (20.0, 7.5), (20.0, 16.5), (12.0, 21.0)], Color32::TRANSPARENT, ink);
|
||||
}
|
||||
// Original bug glyph: rounded body, antennae and three pairs of legs.
|
||||
"bug_report" => {
|
||||
pen.circle(12.0, 13.5, 5.0, Color32::TRANSPARENT, ink);
|
||||
pen.line(&[(12.0, 9.0), (12.0, 18.0)], ink);
|
||||
pen.line(&[(9.5, 8.5), (8.0, 5.0)], ink);
|
||||
pen.line(&[(14.5, 8.5), (16.0, 5.0)], ink);
|
||||
for y in [10.0, 13.5, 17.0] {
|
||||
pen.line(&[(3.0, y), (7.0, y)], ink);
|
||||
pen.line(&[(17.0, y), (21.0, y)], ink);
|
||||
}
|
||||
}
|
||||
// Help: a question mark in a ring.
|
||||
"help" => {
|
||||
pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink);
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{SolveApp, dialogs, help};
|
||||
|
||||
const MAX_BODY: usize = 24_000;
|
||||
const MAX_IMAGE_PIXELS: usize = 16_000_000;
|
||||
const ISSUE_URL: &str = "https://github.com/bherbruck/solvecraft/issues/new";
|
||||
const ISSUE_URL: &str = "https://github.com/bherbruck/solidcraft/issues/new";
|
||||
|
||||
thread_local! {
|
||||
static CAD_INPUT_BLOCKED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
@@ -480,7 +480,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if ui.add_enabled(draft.image.is_some() && app.services.pick_save.is_some(), egui::Button::new("Save screenshot…")).clicked()
|
||||
&& let Some(image) = draft.image.as_ref()
|
||||
&& let Some(path) =
|
||||
app.services.pick_save.as_ref().and_then(|pick| pick(&format!("solvecraft-issue-{}.png", app.shot_token), &["png"]))
|
||||
app.services.pick_save.as_ref().and_then(|pick| pick(&format!("solidcraft-issue-{}.png", app.shot_token), &["png"]))
|
||||
{
|
||||
let result = crate::control::save_screenshot(image, Some(&path));
|
||||
draft.note = if result.get("ok").and_then(serde_json::Value::as_bool) == Some(true) {
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn command_for(app: &SolveApp, key: &str) -> Option<&'static str> {
|
||||
pub fn bind(app: &mut SolveApp, id: &str, key: &str, replace: bool) -> Result<(), String> {
|
||||
let spec = solvecraft_engine::find_command(id).ok_or_else(|| format!("no command `{id}`"))?;
|
||||
if RESERVED.contains(&key) {
|
||||
return Err(format!("{key} is used by SolveCraft itself"));
|
||||
return Err(format!("{key} is used by SolidCraft itself"));
|
||||
}
|
||||
if !key.is_empty() {
|
||||
let others: Vec<&'static str> = commands_for(app, key).into_iter().filter(|o| *o != id).collect();
|
||||
@@ -224,7 +224,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
Some((_, "reset_all")) => reset(app, None),
|
||||
Some((_, "export")) => {
|
||||
let text = crate::toolbar_custom::export(app).to_string();
|
||||
if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f("SolveCraft shortcuts.json", &["json"])) {
|
||||
if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f("SolidCraft shortcuts.json", &["json"])) {
|
||||
app.keymap.message = std::fs::write(&path, text).err().map(|e| format!("export: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"sketch.make_selection": "選択を{linetype}にする",
|
||||
"ui.1_degree_of_freedom": "自由度 1",
|
||||
"ui.3d_sketch": "3D スケッチ",
|
||||
"ui.about": "SolveCraft について",
|
||||
"ui.about": "SolidCraft について",
|
||||
"ui.activate": "アクティブ化",
|
||||
"ui.active_component": "アクティブなコンポーネント",
|
||||
"ui.agent_cursor": "AIエージェントカーソル",
|
||||
|
||||
@@ -616,7 +616,7 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
note(ui, "Anti-aliasing applies the next time SolveCraft starts");
|
||||
note(ui, "Anti-aliasing applies the next time SolidCraft starts");
|
||||
ui.label("Renderer");
|
||||
ui.label(match (&app.viewport.gpu, &app.help.gpu) {
|
||||
(Some(_), Some(g)) => format!("GPU: {g}"),
|
||||
|
||||
@@ -63,7 +63,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
.min_width(420.0)
|
||||
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
|
||||
.show(ctx, |ui| {
|
||||
ui.label("SolveCraft closed before these designs were saved. Their last autosave can be opened.");
|
||||
ui.label("SolidCraft closed before these designs were saved. Their last autosave can be opened.");
|
||||
ui.add_space(6.0);
|
||||
for e in app.recoverable.iter().take(20) {
|
||||
ui.label(RichText::new(&e.name).strong());
|
||||
|
||||
@@ -36,6 +36,13 @@ pub fn promoted(tab: &str, panel: &str) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
fn report_rect(bar: Rect, captions: f32, tabs_start: f32) -> (Rect, bool) {
|
||||
let right = bar.right() - captions - 90.0;
|
||||
let labeled = right - tabs_start >= 340.0;
|
||||
let width = if labeled { 124.0 } else { 28.0 };
|
||||
(Rect::from_min_size(pos2(right - width, bar.top() + 4.0), vec2(width, 26.0)), labeled)
|
||||
}
|
||||
|
||||
pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get();
|
||||
egui::Panel::top("sc_appbar").exact_size(34.0).show_separator_line(false).frame(egui::Frame::NONE.fill(t.app_bar)).show(ui, |ui| {
|
||||
@@ -45,14 +52,20 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::titlebar::drag_area(ui, r);
|
||||
}
|
||||
let left = r.left() + if app.integrated_titlebar { crate::titlebar::TRAFFIC_LIGHTS } else { 0.0 };
|
||||
// At high UI scale, keep the report, Home, Help and window controls reachable.
|
||||
// The theme shortcut remains available in Preferences.
|
||||
let compact_actions = r.right() - left < 640.0;
|
||||
// Brand mark: a small solid block drawn in code.
|
||||
let logo = Rect::from_center_size(pos2(left + 20.0, r.center().y), vec2(20.0, 20.0));
|
||||
icons::paint(ui.painter(), logo, "box", Color32::from_rgb(220, 226, 236), Color32::from_rgb(90, 160, 240), Color32::WHITE);
|
||||
ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolveCraft", FontId::proportional(14.0), t.app_bar_text);
|
||||
let mut x = left + 128.0;
|
||||
if !compact_actions {
|
||||
ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolidCraft", FontId::proportional(14.0), t.app_bar_text);
|
||||
}
|
||||
let mut x = left + if compact_actions { 60.0 } else { 128.0 };
|
||||
let mut click = |ui: &mut egui::Ui, icon: &str, tip: &str| -> bool {
|
||||
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(26.0, 26.0));
|
||||
x += 30.0;
|
||||
let width = if compact_actions { 22.0 } else { 26.0 };
|
||||
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(width, 26.0));
|
||||
x += if compact_actions { 22.0 } else { 30.0 };
|
||||
let resp = ui.interact(br, ui.id().with(("ab", icon)), Sense::click());
|
||||
crate::scenario::publish_handle(&format!("appbar:{icon}"), br.center());
|
||||
if resp.hovered() {
|
||||
@@ -83,7 +96,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
app.home.show(rev);
|
||||
}
|
||||
let theme_tip = if app.ui.dark { app.tr("ui.light") } else { app.tr("ui.dark") };
|
||||
if click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
||||
if !compact_actions && click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip) {
|
||||
app.ui.dark = !app.ui.dark;
|
||||
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
|
||||
}
|
||||
@@ -109,7 +122,34 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
// Document tabs.
|
||||
let captions = if app.custom_titlebar { crate::titlebar::WIDTH } else { 0.0 };
|
||||
crate::documents::tabs(app, ui, r, x + 16.0, r.right() - captions - 90.0);
|
||||
let tabs_start = x + 16.0;
|
||||
let (report, report_label) = report_rect(r, captions, tabs_start);
|
||||
let tabs_end = report.left() - 8.0;
|
||||
// Narrow title bars can fit only part of the tab strip. Its hit regions must not
|
||||
// extend into the persistent report and window controls.
|
||||
let tabs_rect = Rect::from_min_max(pos2(tabs_start.min(tabs_end), r.top()), pos2(tabs_end, r.bottom()));
|
||||
let mut tabs_ui = ui.new_child(egui::UiBuilder::new().max_rect(tabs_rect));
|
||||
tabs_ui.set_clip_rect(tabs_rect.intersect(ui.clip_rect()));
|
||||
crate::documents::tabs(app, &mut tabs_ui, r, tabs_start, tabs_end);
|
||||
let response = ui.interact(report, ui.id().with("sc_report_button"), Sense::click());
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Report a bug"));
|
||||
crate::scenario::publish_handle("titlebar:report", report.center());
|
||||
ui.painter().rect_filled(report, 4.0, if response.hovered() || response.has_focus() { t.hover } else { t.app_bar });
|
||||
ui.painter().rect_stroke(report, 4.0, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
|
||||
let icon = Rect::from_center_size(pos2(report.left() + 14.0, report.center().y), vec2(18.0, 18.0));
|
||||
icons::paint(ui.painter(), icon, "bug_report", t.app_bar_text, t.app_bar_text, t.app_bar);
|
||||
if report_label {
|
||||
ui.painter().text(
|
||||
pos2(report.left() + 29.0, report.center().y),
|
||||
Align2::LEFT_CENTER,
|
||||
"Report a bug",
|
||||
FontId::proportional(13.0),
|
||||
t.app_bar_text,
|
||||
);
|
||||
}
|
||||
if response.on_hover_text("Report a bug — review a screenshot and open a GitHub draft").clicked() {
|
||||
crate::help::run(app, &ui.ctx().clone(), "report");
|
||||
}
|
||||
// Preferences, at the top right like Fusion's profile menu (also Ctrl+,).
|
||||
let gear = Rect::from_center_size(pos2(r.right() - captions - 70.0, r.center().y), vec2(24.0, 24.0));
|
||||
let gresp = ui.interact(gear, ui.id().with("sc_prefs_button"), Sense::click());
|
||||
@@ -612,3 +652,99 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod header_report_tests {
|
||||
use super::*;
|
||||
|
||||
fn captured_frame(h: &mut crate::scenario::Harness) {
|
||||
let token = h.app.shot_token;
|
||||
crate::issue_report::receive(&mut h.app, token, std::sync::Arc::new(egui::ColorImage::new([2, 2], vec![Color32::GREEN; 4])));
|
||||
h.frames(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_report_reserves_controls_at_normal_and_double_scale_widths() {
|
||||
for physical_width in [900.0, 1400.0] {
|
||||
for scale in [1.0, 2.0] {
|
||||
let width = physical_width / scale;
|
||||
let bar = Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 34.0));
|
||||
for captions in [0.0, crate::titlebar::WIDTH] {
|
||||
let (report, labeled) = report_rect(bar, captions, 354.0);
|
||||
assert!(bar.contains_rect(report));
|
||||
let prefs = Rect::from_center_size(pos2(width - captions - 70.0, 17.0), vec2(24.0, 24.0));
|
||||
assert!(!report.intersects(prefs));
|
||||
assert!(report.right() < width - captions - 82.0);
|
||||
assert_eq!(labeled, width - captions - 90.0 - 354.0 >= 340.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_report_actual_click_opens_capture_without_changing_healthy_design() {
|
||||
for width in [450.0, 700.0, 900.0, 1400.0] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.custom_titlebar = true;
|
||||
h.app.run("solid.box", json!({"length":35,"width":12,"height":4})).unwrap();
|
||||
h.call("ui.resize", json!({"width":width,"height":720}));
|
||||
h.frames(3);
|
||||
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision);
|
||||
let point = h.widget("Report a bug").expect("compact and full buttons retain an accessible name");
|
||||
h.step(&json!({"click":point})).unwrap();
|
||||
assert!(h.app.issue_report.draft.is_some(), "width {width}: header click must reach the report");
|
||||
assert!(crate::issue_report::pending(&h.app));
|
||||
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||
captured_frame(&mut h);
|
||||
// Existing review content scrolls vertically when the window is narrow.
|
||||
if width < 700.0 {
|
||||
let [x, y] = h.widget("Close draft").unwrap();
|
||||
h.call("ui.scroll", json!({"x":x,"y":y - 60.0,"delta":-1200}));
|
||||
// Let the scroll animation finish before locating the click target.
|
||||
h.frames(60);
|
||||
}
|
||||
h.step(&json!({"widget":"Close draft"})).unwrap();
|
||||
assert!(h.app.issue_report.draft.is_none(), "width {width}: review closes through its actual button");
|
||||
h.step(&json!({"click":{"handle":"appbar:home"}})).unwrap();
|
||||
assert!(h.app.home.open, "width {width}: Home remains reachable beside Report");
|
||||
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_report_preserves_new_dimension_draft_and_single_commit() {
|
||||
crate::dim_view::cancel();
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points":[[0,0],[20,0]]})).unwrap();
|
||||
h.call("ui.view", json!({"view":"top"}));
|
||||
h.app.cam.distance = 80.0;
|
||||
h.app.cam.target = solvecraft_engine::geom::Vec3::new(10.0, 0.0, 0.0);
|
||||
h.settle();
|
||||
h.step(&json!({"start":"sketch.dimension"})).unwrap();
|
||||
for point in [[0, 0, 0], [20, 0, 0], [10, 5, 0]] {
|
||||
h.step(&json!({"click":{"world":point}})).unwrap();
|
||||
}
|
||||
h.step(&json!({"text":"30 mm"})).unwrap();
|
||||
let before = (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision);
|
||||
h.step(&json!({"widget":"Report a bug"})).unwrap();
|
||||
h.frames(3);
|
||||
assert!(h.app.issue_report.draft.is_some() && crate::dim_view::editing());
|
||||
captured_frame(&mut h);
|
||||
h.ctx.memory_mut(|m| m.request_focus(egui::Id::new("issue_report_body")));
|
||||
h.step(&json!({"text":"Describe this dimension problem."})).unwrap();
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||
h.step(&json!({"widget":"Close draft"})).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(h.ctx.memory(|m| m.focused()), Some(egui::Id::new("sc_dim_edit")));
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert!(!crate::dim_view::editing());
|
||||
assert_eq!(h.app.session.undo.len(), before.1 + 1);
|
||||
assert_eq!(h.app.session.doc.param("d1").unwrap().expr, "30 mm");
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(h.app.session.doc.to_json(), before.0);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ pub fn export(app: &SolveApp) -> Value {
|
||||
|
||||
pub fn import(app: &mut SolveApp, v: &Value) -> Result<(), String> {
|
||||
if v.get("format").and_then(Value::as_str) != Some("solvecraft-shortcuts/1") {
|
||||
return Err("not a SolveCraft shortcuts file".into());
|
||||
return Err("not a SolidCraft shortcuts file".into());
|
||||
}
|
||||
if let Some(s) = v.get("shortcuts") {
|
||||
app.keymap.load(s);
|
||||
|
||||
Reference in new issue
Block a user