feat(app): add fixed Start tab with welcome page and recent documents

A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.

- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
  Start tab. Recents persist to disk
  (%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
  on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
  to 20 entries, saved on every open and rehydrated at boot.
  Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
  retired; the same actions surface on the welcome page instead. The
  Donate button is the primary call-to-action — solid #B03020 brand
  fill with a soft brand-tinted ambient glow halo behind the action row
  (Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
  selection markers) are suppressed on the Start tab so its clicks
  don't trigger CAD commands. OpenFile's "current_is_empty" check
  refuses to replace the Start tab, and TabClose ignores requests on
  it.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
HakanandClaude Opus 4.7 committed 2026-05-21 16:19:31 +03:00
1 parent 3fb23ae31b
commit 35acdb2dc8
10 files changed
+515 -72

No files matched your search

+61 -1
View File
@@ -92,7 +92,20 @@ impl AppMenu {
pub fn push_recent(&mut self, path: PathBuf) {
self.recent.retain(|r| r != &path);
self.recent.insert(0, path);
self.recent.truncate(10);
self.recent.truncate(20);
// Best-effort persist; silent on failure (read-only home, full disk).
let _ = save_recents(&self.recent);
}
/// Drop a path from the recents list (manual removal from Start page).
pub fn remove_recent(&mut self, path: &std::path::Path) {
self.recent.retain(|r| r.as_path() != path);
let _ = save_recents(&self.recent);
}
/// Rehydrate the recents list from disk. Call once at app boot.
pub fn load_persistent_recents(&mut self) {
self.recent = load_recents();
}
pub fn toggle(&mut self) {
@@ -484,3 +497,50 @@ const EXIT_BTN: Color = Color {
b: 0.10,
a: 1.0,
};
// ── Recent-files persistence ─────────────────────────────────────────────
//
// Plain-text format, one path per line, newest first. Lives next to other
// per-user config so we don't pull in a TOML/JSON crate just for this.
fn recents_file_path() -> Option<PathBuf> {
let base: PathBuf = if cfg!(target_os = "windows") {
std::env::var_os("APPDATA").map(PathBuf::from)?
} else if cfg!(target_os = "macos") {
let home = std::env::var_os("HOME")?;
let mut p = PathBuf::from(home);
p.push("Library");
p.push("Application Support");
p
} else if let Some(d) = std::env::var_os("XDG_CONFIG_HOME") {
PathBuf::from(d)
} else {
let home = std::env::var_os("HOME")?;
let mut p = PathBuf::from(home);
p.push(".config");
p
};
let mut p = base;
p.push("H7CAD");
Some(p.join("recent.txt"))
}
fn save_recents(list: &[PathBuf]) -> std::io::Result<()> {
let Some(path) = recents_file_path() else { return Ok(()); };
if let Some(dir) = path.parent() { std::fs::create_dir_all(dir)?; }
let body: String = list
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("\n");
std::fs::write(path, body)
}
fn load_recents() -> Vec<PathBuf> {
let Some(path) = recents_file_path() else { return vec![]; };
let Ok(body) = std::fs::read_to_string(path) else { return vec![]; };
body.lines()
.filter(|l| !l.trim().is_empty())
.map(PathBuf::from)
.collect()
}