Skip to main content

Mountain/Binary/Build/
WindowBuild.rs

1//! # Window Build Module
2//!
3//! Creates and configures the main application window.
4
5use tauri::{App, WebviewUrl, WebviewWindowBuilder, Wry};
6
7use crate::IPC::WindServiceHandlers::Utilities::RecentlyOpened::ReadRecentlyOpened;
8
9/// Creates and configures the main application window.
10///
11/// # Arguments
12///
13/// * `Application` - The Tauri application instance
14/// * `LocalhostUrl` - The localhost URL for the webview content
15///
16/// # Returns
17///
18/// A configured `WebviewWindow<Wry>` instance.
19///
20/// # Platform-Specific Behavior
21///
22/// - **macOS**: `TitleBarStyle::Overlay` + `hidden_title(true)` keeps the
23///   traffic-light buttons at the top-left but hides the native title bar
24///   strip, so VS Code's custom titlebar (which has `-webkit-app-region: drag`
25///   baked into its CSS) lights up the entire top row as a drag region. The
26///   previous `maximized(true)` was the direct cause of "can't drag the editor
27///   around" - maximized macOS windows are pinned to the screen and refuse all
28///   drag events, regardless of `app-region` CSS.
29/// - **Windows / Linux**: `decorations(false)` keeps the window chrome-less so
30///   the workbench draws its own. We still start `resizable(true)` so the
31///   window can be moved by the drag region.
32/// - **Debug builds**: DevTools auto-open.
33pub fn WindowBuild(Application:&mut App, LocalhostUrl:String) -> tauri::WebviewWindow<Wry> {
34	// Restore the most-recently-opened folder so the webview boots
35	// directly into the workspace. Without this, every launch lands
36	// on the Welcome tab, the user clicks "Open Folder", and the
37	// `pickFolderAndOpen` handler fires `Window.navigate()` - a hard
38	// reload that wipes workbench state mid-initialisation and is
39	// the direct cause of the purple splash flash, stuttering paint,
40	// empty `@builtin` sidebar, and broken keybindings (every layer
41	// has to boot twice and the second pass often loses references
42	// to the first). Using `?folder=...` in the initial URL skips
43	// that destructive round-trip.
44	let InitialUrl = BuildInitialUrl(&LocalhostUrl);
45	let WindowUrl = WebviewUrl::External(InitialUrl.parse().expect("FATAL: Failed to parse initial webview URL"));
46
47	// Configure window builder with base settings.
48	//
49	// `visible(false)` is the hidden-until-ready pattern. Tauri's default
50	// is to show the window the instant it's built, which paints the native
51	// chrome + Base.astro's `#1e1e1e` inline background + VS Code theme CSS
52	// + workbench DOM in four separate repaints over the first ~200 ms -
53	// observed as the "purple/dark flash" and panel-pop flicker.
54	//
55	// Mountain shows the window explicitly when the frontend's
56	// `lifecycle:advancePhase(3)` (Restored) arrives, which fires after
57	// `.monaco-workbench` is attached and the first frame is ready. A 3 s
58	// safety timer in `AppLifecycle` guarantees the window appears even if
59	// Sky crashes before signalling phase 3.
60	let mut WindowBuilder = WebviewWindowBuilder::new(Application, "main", WindowUrl)
61		.use_https_scheme(false)
62		.initialization_script("")
63		.zoom_hotkeys_enabled(true)
64		.browser_extensions_enabled(false)
65		// macOS first-responder: by default WKWebView swallows the
66		// first click on an unfocused window as a "make me key"
67		// no-op and the click never reaches the inner content. With
68		// `Inspect=1` running DevTools alongside the main window
69		// every switch-back to the editor needed two clicks - first
70		// to focus the NSWindow, second to actually focus the
71		// Monaco textarea - which the user reports as "I clicked,
72		// I'm typing, nothing's happening". `accept_first_mouse`
73		// flips the responder chain so the first click already
74		// reaches WKWebView's content and the textarea picks up
75		// keyboard input immediately.
76		.accept_first_mouse(true)
77		.title("Mountain")
78		.resizable(true)
79		.inner_size(1400.0, 900.0)
80		.shadow(true)
81		.visible(false);
82
83	#[cfg(target_os = "macos")]
84	{
85		// Overlay style lets VS Code's custom titlebar paint behind the
86		// traffic-light buttons. `hidden_title(true)` suppresses the OS
87		// title text so it doesn't collide with the workbench menubar.
88		// `decorations(true)` is REQUIRED for the traffic lights to
89		// render - turning decorations off also removes the buttons and
90		// breaks the native drag + resize handles entirely on macOS.
91		WindowBuilder = WindowBuilder
92			.title_bar_style(tauri::TitleBarStyle::Overlay)
93			.hidden_title(true)
94			.decorations(true);
95	}
96
97	#[cfg(any(target_os = "windows", target_os = "linux"))]
98	{
99		// Keep chrome-less on non-macOS - the workbench provides its
100		// own window controls via the WindowsTitleBar / LinuxTitleBar
101		// components. Drag works via `-webkit-app-region: drag` on the
102		// titlebar element.
103		WindowBuilder = WindowBuilder.decorations(false);
104	}
105
106	// Build the main window
107	let MainWindow = WindowBuilder.build().expect("FATAL: Main window build failed");
108
109	// DevTools auto-open lives in `Binary/Main/AppLifecycle.rs:174`
110	// (gated on `cfg(debug_assertions)`, with a `[Window] Debug build:
111	// opening DevTools.` log line). Calling `open_devtools()` here as
112	// well opened a SECOND DevTools window on every debug launch -
113	// reported as "two DevTools" after the last rebuild. Single-source
114	// the call to AppLifecycle so the log line and the window match.
115
116	MainWindow
117}
118
119/// Build the initial webview URL, optionally appending `?folder=<path>`
120/// when `~/.land/workspaces/RecentlyOpened.json` has an entry for the
121/// previous session's workspace. Falls back to plain `index.html` if
122/// the file is missing, malformed, or has no resolvable path.
123///
124/// The returned string is already URL-encoded and safe to feed to
125/// `WebviewUrl::External`.
126fn BuildInitialUrl(LocalhostUrl:&str) -> String {
127	let Base = format!("{}/index.html", LocalhostUrl);
128
129	let Recent = match ReadRecentlyOpened() {
130		Ok(Value) => Value,
131		Err(_) => return Base,
132	};
133	let Workspaces = match Recent.get("workspaces").and_then(|V| V.as_array()) {
134		Some(Array) if !Array.is_empty() => Array,
135		_ => return Base,
136	};
137
138	// VS Code's Recently-Opened record can store the folder under a few
139	// different shapes depending on whether the entry came from the
140	// extension host, the workbench, or a `$deltaWorkspaceFolders`
141	// broadcast. Probe them in the same priority order the workbench
142	// itself uses in `getRecentlyOpenedWorkspaces`.
143	let Probe = |Entry:&serde_json::Value| -> Option<String> {
144		// Mountain's own writer emits `{ uri: "file://…", label }` (see
145		// `RecentlyOpened.json` on a freshly closed window). VS Code's
146		// historical `folderUri` / `workspace.configPath` shapes are kept
147		// as fallbacks so imported profiles and third-party writers keep
148		// working.
149		if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
150			return Some(Uri.to_string());
151		}
152		if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
153			return Some(Uri.to_string());
154		}
155		if let Some(Path) = Entry.get("folderUri").and_then(|V| V.get("path")).and_then(|V| V.as_str()) {
156			return Some(Path.to_string());
157		}
158		if let Some(Path) = Entry
159			.get("workspace")
160			.and_then(|V| V.get("configPath"))
161			.and_then(|V| V.get("path"))
162			.and_then(|V| V.as_str())
163		{
164			return Some(Path.to_string());
165		}
166		None
167	};
168
169	let FolderPath = match Workspaces.iter().find_map(Probe) {
170		Some(Path) => Path,
171		None => return Base,
172	};
173
174	// Strip any `file://` scheme so the query param is a plain path
175	// the workbench will stringify into a `file:` URI itself; leaving
176	// the scheme in doubles up and breaks the URL-decode on the other
177	// side (observed as the second `?folder=` boot path appearing as
178	// `file:/Volumes/...` in `wb:boot`).
179	let WithoutScheme = FolderPath.strip_prefix("file://").unwrap_or(FolderPath.as_str()).to_string();
180	// RecentlyOpened.json stores workspace URIs with a trailing slash
181	// (`file:///Volumes/.../Mountain/`). Drop it before encoding into
182	// the URL so the workbench-side `URI.revive({ scheme: "file",
183	// path: <param> })` produces a folder URI that matches the
184	// workbench's own `URI.from(<file>)` results - which never carry
185	// a trailing slash on the parent directory. The mismatch caused
186	// `IUriIdentityService.extUri.relativePath` to return absolute
187	// paths and breadcrumbs / quick-pick / Problems-panel labels to
188	// render `/Volumes/CORSAIR/...` instead of the workspace-relative
189	// short form. Preserve `/` itself when the path IS root (vanishing
190	// edge case but cheap to guard).
191	let TrailingTrimmed = WithoutScheme.trim_end_matches('/');
192	let Normalised = if TrailingTrimmed.is_empty() {
193		"/".to_string()
194	} else {
195		TrailingTrimmed.to_string()
196	};
197	if !std::path::Path::new(&Normalised).is_dir() {
198		return Base;
199	}
200
201	let Encoded = url::form_urlencoded::Serializer::new(String::new())
202		.append_pair("folder", &Normalised)
203		.finish();
204	format!("{}/?{}", LocalhostUrl, Encoded)
205}