From aa9755d34d210f37011e94494701290cfec2cf96 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:57:13 +0900 Subject: [PATCH 1/5] feat(window-wrapper): add generic window registry for focus-existing-or-open-new Extracts the ad-hoc SettingsWindowHandle pattern into a reusable WindowRegistry keyed by window-kind, so any window type gets duplicate-prevention for free. Also fixes focus: reopening an already-open window now calls Window::activate_window() to bring it forward, instead of silently no-oping. --- crates/app/src/main.rs | 24 ++++++------- crates/settings/src/lib.rs | 9 ----- crates/window-wrapper/src/lib.rs | 2 ++ crates/window-wrapper/src/window_registry.rs | 37 ++++++++++++++++++++ 4 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 crates/window-wrapper/src/window_registry.rs diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index fd82a1fd..54501435 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -9,10 +9,14 @@ mod title_items; use gpui::*; use gpui_component::{Root, TitleBar, v_flex}; use settings::{ - AppSettings, MainWindowBounds, SettingsPersistence, SettingsWindow, SettingsWindowHandle, - SettingsWriter, load_app_settings, + AppSettings, MainWindowBounds, SettingsPersistence, SettingsWindow, SettingsWriter, + load_app_settings, }; -use window_wrapper::{OpenBrowser, WindowLock, status_bar::StatusBar, title_bar::AppTitleBar}; +use window_wrapper::{ + OpenBrowser, WindowLock, WindowRegistry, status_bar::StatusBar, title_bar::AppTitleBar, +}; + +const SETTINGS_WINDOW_KIND: &str = "settings"; use crate::app_settings::build_pages; use crate::{ @@ -119,17 +123,11 @@ fn main() { cx.set_global(SettingsPersistence { writer: Some(SettingsWriter::start()), }); - cx.set_global(SettingsWindowHandle::default()); + cx.set_global(WindowRegistry::default()); cx.on_action(|_: &OpenSettings, cx| { - let state = cx.global::(); - - if let Some(handle) = &state.handle { - if handle.update(cx, |_, _, _| {}).is_ok() { - return; - } else { - cx.global_mut::().handle = None; - } + if WindowRegistry::focus_or_clear(SETTINGS_WINDOW_KIND, cx) { + return; } let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx); let window_options = WindowOptions { @@ -147,7 +145,7 @@ fn main() { if let Ok(window_handle) = result { cx.update(|cx| { - cx.global_mut::().handle = Some(window_handle.into()); + WindowRegistry::register(SETTINGS_WINDOW_KIND, window_handle.into(), cx); }) .ok(); } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index c4f5ce37..eefbd23e 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -425,15 +425,6 @@ impl AppSettings { } } -// --- Settings Window State --- - -#[derive(Clone, Default)] -pub struct SettingsWindowHandle { - pub handle: Option, -} - -impl Global for SettingsWindowHandle {} - // --- Settings Window --- pub struct SettingsWindow { diff --git a/crates/window-wrapper/src/lib.rs b/crates/window-wrapper/src/lib.rs index 9b2085f1..0a609327 100644 --- a/crates/window-wrapper/src/lib.rs +++ b/crates/window-wrapper/src/lib.rs @@ -1,10 +1,12 @@ pub mod bar; pub mod status_bar; pub mod title_bar; +pub mod window_registry; pub use bar::{BarItems, BarRegistry}; use gpui::*; pub use gpui_component::TitleBar; +pub use window_registry::WindowRegistry; use schemars::JsonSchema; use serde::Deserialize; diff --git a/crates/window-wrapper/src/window_registry.rs b/crates/window-wrapper/src/window_registry.rs new file mode 100644 index 00000000..859caa1c --- /dev/null +++ b/crates/window-wrapper/src/window_registry.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use gpui::{AnyWindowHandle, App, Global}; + +/// Generic focus-existing-or-open-new registry, keyed by a window-kind string +/// (e.g. "settings", "project-creation"), so each kind only ever has one live +/// window: a second request to open it focuses the existing instance instead +/// of spawning a duplicate. +#[derive(Clone, Default)] +pub struct WindowRegistry { + handles: HashMap<&'static str, AnyWindowHandle>, +} + +impl Global for WindowRegistry {} + +impl WindowRegistry { + /// If a live window is registered under `kind`, focus it and return true. + /// If the registered handle is stale (window already closed), clears it + /// and returns false so the caller can open a fresh window. + pub fn focus_or_clear(kind: &'static str, cx: &mut App) -> bool { + let Some(handle) = cx.global::().handles.get(kind).copied() else { + return false; + }; + + let focused = handle + .update(cx, |_, window, _| window.activate_window()) + .is_ok(); + if !focused { + cx.global_mut::().handles.remove(kind); + } + focused + } + + pub fn register(kind: &'static str, handle: AnyWindowHandle, cx: &mut App) { + cx.global_mut::().handles.insert(kind, handle); + } +} From 17a014bfd487d906e3256018824a1a214d2a3e7e Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:36:54 +0900 Subject: [PATCH 2/5] debug(app): add temporary multi-window manual test scaffolding for ASNT-13 Adds crates/app/src/debug.rs: a mock startup window with a button to open the real main window, and a "Debug: Open Settings + Project" menu action that opens Settings plus a stub project window together, so WindowRegistry dedup/focus can be exercised by hand across multiple window kinds at once. Everything is isolated behind DEBUG comments and marked for removal once ASNT-13 is manually verified, before ASNT-15 replaces the stub with the real project-creation wizard. --- crates/app/src/app_menus.rs | 4 ++ crates/app/src/debug.rs | 139 ++++++++++++++++++++++++++++++++++++ crates/app/src/main.rs | 85 ++++++++++++---------- 3 files changed, 190 insertions(+), 38 deletions(-) create mode 100644 crates/app/src/debug.rs diff --git a/crates/app/src/app_menus.rs b/crates/app/src/app_menus.rs index 15249389..9aa3c56b 100644 --- a/crates/app/src/app_menus.rs +++ b/crates/app/src/app_menus.rs @@ -1,5 +1,7 @@ use gpui::*; +use crate::debug::DebugOpenBoth; // DEBUG (ASNT-13 manual test scaffolding) + actions!(nav, [OpenSettings, Quit]); pub fn app_menus() -> Vec { @@ -7,6 +9,8 @@ pub fn app_menus() -> Vec { name: "App".into(), items: vec![ MenuItem::action("Settings", OpenSettings), + // DEBUG (ASNT-13 manual test scaffolding): remove once ASNT-13 is verified. + MenuItem::action("Debug: Open Settings + Project", DebugOpenBoth), MenuItem::Separator, MenuItem::action("Quit", Quit), ], diff --git a/crates/app/src/debug.rs b/crates/app/src/debug.rs new file mode 100644 index 00000000..fbbc7275 --- /dev/null +++ b/crates/app/src/debug.rs @@ -0,0 +1,139 @@ +//! DEBUG (ASNT-13 manual test scaffolding — delete this module and its two +//! call sites in `main.rs`/`app_menus.rs` once ASNT-13's multi-window +//! behavior has been manually verified, and before ASNT-15 lands the real +//! project-creation wizard): a mock startup window with a button to open the +//! real main window, plus a menu action that opens Settings and a stub +//! "project" window together so two window kinds can be exercised live at +//! once. + +use gpui::*; +use gpui_component::{Root, TitleBar, button::Button, v_flex}; +use settings::AppSettings; +use window_wrapper::WindowRegistry; + +use crate::{App, open_settings_window}; + +actions!(debug, [DebugOpenBoth]); + +const MAIN_WINDOW_KIND: &str = "main"; +const DEBUG_PROJECT_KIND: &str = "debug-project-stub"; + +/// The real main-window-opening body, previously inline at the end of +/// `main()`. Now reusable so the mock startup window's button can open it on +/// demand, with the same focus-existing-or-open-new dedup Settings already +/// gets from `WindowRegistry`. +pub fn open_main_window(cx: &mut App) { + if WindowRegistry::focus_or_clear(MAIN_WINDOW_KIND, cx) { + return; + } + + let (main_bounds, main_display) = AppSettings::get(cx).main_window_startup_placement(cx); + let window_options = WindowOptions { + titlebar: Some(TitleBar::title_bar_options()), + window_bounds: Some(WindowBounds::Windowed(main_bounds)), + display_id: main_display, + window_min_size: Some(Size::new(px(520.0), px(300.0))), + ..Default::default() + }; + + cx.spawn(async move |cx| { + let result = cx.open_window(window_options, |window, cx| { + let view = cx.new(|cx| App::new(window, cx)); + cx.new(|cx| Root::new(view, window, cx)) + }); + + if let Ok(window_handle) = result { + cx.update(|cx| { + WindowRegistry::register(MAIN_WINDOW_KIND, window_handle.into(), cx); + }) + .ok(); + } + }) + .detach(); +} + +/// Bare-bones window standing in for the not-yet-built project-creation +/// wizard (ASNT-15) — just enough to be a second, visibly distinct window +/// kind to test alongside Settings. +struct DebugProjectStub; + +impl Render for DebugProjectStub { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .items_center() + .justify_center() + .child("Project Creation (debug stub) — ASNT-15 not built yet") + } +} + +fn open_debug_project_stub(cx: &mut App) { + if WindowRegistry::focus_or_clear(DEBUG_PROJECT_KIND, cx) { + return; + } + + let bounds = Bounds::centered(None, size(px(500.0), px(300.0)), cx); + let window_options = WindowOptions { + titlebar: Some(TitleBar::title_bar_options()), + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }; + + cx.spawn(async move |cx| { + let result = cx.open_window(window_options, |window, cx| { + let view = cx.new(|_| DebugProjectStub); + cx.new(|cx| Root::new(view, window, cx)) + }); + + if let Ok(window_handle) = result { + cx.update(|cx| { + WindowRegistry::register(DEBUG_PROJECT_KIND, window_handle.into(), cx); + }) + .ok(); + } + }) + .detach(); +} + +/// Registers the `DebugOpenBoth` menu action: opens Settings and the stub +/// project window at once, so their `WindowRegistry` dedup can be exercised +/// with two windows live simultaneously. +pub fn register_debug_menu_action(cx: &mut App) { + cx.on_action(|_: &DebugOpenBoth, cx| { + open_settings_window(cx); + open_debug_project_stub(cx); + }); +} + +/// Mock "project management" window shown at startup instead of the real +/// main window. Its one button opens the real main window and closes this +/// one. +pub struct DebugMockProjectWindow; + +impl Render for DebugMockProjectWindow { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .items_center() + .justify_center() + .gap_4() + .child("Debug: mock project management window") + .child( + Button::new("open-main-project") + .label("Open Main Project") + .on_click(|_, window, cx| { + open_main_window(cx); + window.remove_window(); + }), + ) + } +} + +pub fn mock_startup_window_options(cx: &mut App) -> WindowOptions { + let bounds = Bounds::centered(None, size(px(400.0), px(200.0)), cx); + WindowOptions { + titlebar: Some(TitleBar::title_bar_options()), + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + } +} diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 54501435..a2b750c3 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -2,6 +2,7 @@ mod actions; mod app_menus; mod app_settings; mod components; +mod debug; // DEBUG (ASNT-13 manual test scaffolding) — see crates/app/src/debug.rs mod helpers; mod status_items; mod title_items; @@ -22,12 +23,44 @@ use crate::app_settings::build_pages; use crate::{ actions::{ToggleBottomDock, ToggleLeftDock, ToggleRightDock}, app_menus::{OpenSettings, Quit, app_menus}, + debug::register_debug_menu_action, // DEBUG (ASNT-13 manual test scaffolding) status_items::build_status_bar_registry, title_items::build_title_bar_registry, }; use gpui_component::dock::DockPlacement; use workspace::Workspace; +/// Opens the Settings window, focusing the existing one if it's already open. +/// Shared by the real `OpenSettings` action and the debug menu's +/// open-two-windows-at-once action (see `crate::debug`). +pub(crate) fn open_settings_window(cx: &mut App) { + if WindowRegistry::focus_or_clear(SETTINGS_WINDOW_KIND, cx) { + return; + } + let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx); + let window_options = WindowOptions { + titlebar: Some(TitleBar::title_bar_options()), + window_bounds: Some(WindowBounds::Windowed(bounds)), + window_min_size: Some(Size::new(px(600.0), px(400.0))), + ..Default::default() + }; + + cx.spawn(async move |cx| { + let result = cx.open_window(window_options, |window, cx| { + let view = cx.new(|cx| SettingsWindow::new(window, cx, build_pages)); + cx.new(|cx| Root::new(view, window, cx)) + }); + + if let Ok(window_handle) = result { + cx.update(|cx| { + WindowRegistry::register(SETTINGS_WINDOW_KIND, window_handle.into(), cx); + }) + .ok(); + } + }) + .detach(); +} + pub struct App { workspace: Entity, status_bar: Entity, @@ -119,39 +152,12 @@ fn main() { // Settings ------------------------------------ let settings = load_app_settings().unwrap_or_default(); cx.set_global(settings); - let (main_bounds, main_display) = AppSettings::get(cx).main_window_startup_placement(cx); cx.set_global(SettingsPersistence { writer: Some(SettingsWriter::start()), }); cx.set_global(WindowRegistry::default()); - cx.on_action(|_: &OpenSettings, cx| { - if WindowRegistry::focus_or_clear(SETTINGS_WINDOW_KIND, cx) { - return; - } - let bounds = Bounds::centered(None, size(px(1000.0), px(800.0)), cx); - let window_options = WindowOptions { - titlebar: Some(TitleBar::title_bar_options()), - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_min_size: Some(Size::new(px(600.0), px(400.0))), - ..Default::default() - }; - - cx.spawn(async move |cx| { - let result = cx.open_window(window_options, |window, cx| { - let view = cx.new(|cx| SettingsWindow::new(window, cx, build_pages)); - cx.new(|cx| Root::new(view, window, cx)) - }); - - if let Ok(window_handle) = result { - cx.update(|cx| { - WindowRegistry::register(SETTINGS_WINDOW_KIND, window_handle.into(), cx); - }) - .ok(); - } - }) - .detach(); - }); + cx.on_action(|_: &OpenSettings, cx| open_settings_window(cx)); // ---------------------------------------------- cx.set_menus(app_menus()); @@ -167,19 +173,22 @@ fn main() { cx.on_action(|_: &Quit, cx| { cx.quit(); }); - let min_size = Size::new(px(520.0), px(300.0)); - - let window_options = WindowOptions { - titlebar: Some(TitleBar::title_bar_options()), - window_bounds: Some(WindowBounds::Windowed(main_bounds)), - display_id: main_display, - window_min_size: Some(min_size), - ..Default::default() - }; + // DEBUG (ASNT-13 manual test scaffolding): opens Settings + a stub + // project window together via the "Debug: Open Settings + Project" + // menu item. See crates/app/src/debug.rs. + register_debug_menu_action(cx); + + // DEBUG (ASNT-13 manual test scaffolding): the startup window is a mock + // project-management window instead of the real main window, so the real + // main window, Settings, and the stub project window can all be opened + // independently to test WindowRegistry. Its button calls + // debug::open_main_window. Revert this block to open `App::new` directly + // once ASNT-13 has been manually verified. + let window_options = debug::mock_startup_window_options(cx); cx.spawn(async move |cx| { cx.open_window(window_options, |window, cx| { - let view = cx.new(|cx| App::new(window, cx)); + let view = cx.new(|_| debug::DebugMockProjectWindow); cx.new(|cx| Root::new(view, window, cx)) }) .expect("Failed to open window"); From 847af37432f0251df065038e37d3b26358d1c0b3 Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:49:01 +0900 Subject: [PATCH 3/5] fix(app): resolve App/gpui::App name collision breaking the build crate::App (this crate's main window struct) and gpui::App (the context type) share a name. An explicit `use crate::App` in debug.rs, and an untyped `&mut App` param in main.rs's extracted open_settings_window, both silently resolved to the local struct instead of gpui::App since explicit imports/same-module items win over glob imports regardless of source order. Fixes it by aliasing the local import and spelling out gpui::App where needed. --- crates/app/src/debug.rs | 7 +++++-- crates/app/src/main.rs | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/app/src/debug.rs b/crates/app/src/debug.rs index fbbc7275..b3578132 100644 --- a/crates/app/src/debug.rs +++ b/crates/app/src/debug.rs @@ -11,7 +11,10 @@ use gpui_component::{Root, TitleBar, button::Button, v_flex}; use settings::AppSettings; use window_wrapper::WindowRegistry; -use crate::{App, open_settings_window}; +// Aliased: a bare `App` import here would shadow `gpui::App` (from the glob +// import above) for the whole module, since explicit imports win over globs. +use crate::App as MainWindow; +use crate::open_settings_window; actions!(debug, [DebugOpenBoth]); @@ -38,7 +41,7 @@ pub fn open_main_window(cx: &mut App) { cx.spawn(async move |cx| { let result = cx.open_window(window_options, |window, cx| { - let view = cx.new(|cx| App::new(window, cx)); + let view = cx.new(|cx| MainWindow::new(window, cx)); cx.new(|cx| Root::new(view, window, cx)) }); diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index a2b750c3..1a7f650a 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -33,7 +33,10 @@ use workspace::Workspace; /// Opens the Settings window, focusing the existing one if it's already open. /// Shared by the real `OpenSettings` action and the debug menu's /// open-two-windows-at-once action (see `crate::debug`). -pub(crate) fn open_settings_window(cx: &mut App) { +// `gpui::App`, spelled out: a bare `App` here would resolve to this file's own +// `pub struct App` below, not the GPUI context type — local items always win +// over glob imports regardless of source order. +pub(crate) fn open_settings_window(cx: &mut gpui::App) { if WindowRegistry::focus_or_clear(SETTINGS_WINDOW_KIND, cx) { return; } From 9d7659c36a73dbec01603d4fc0fb03cfa15b9ebd Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:47:02 +0530 Subject: [PATCH 4/5] chore: fix pre-existing fmt/clippy drift blocking CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unrelated to ASNT-13, but was blocking this PR's CI entirely since the format-check step runs before clippy/build/test and was already failing on main: rustfmt drift in crates/table (panel.rs, selection.rs) and a clippy redundant-closure lint in status_items/mod.rs. Verified with the exact CI command (cargo fmt --all --check, cargo build, cargo clippy --workspace --all-targets -- -D warnings -A dead_code) — all pass now. --- crates/app/src/status_items/mod.rs | 2 +- crates/table/src/panel.rs | 5 ++--- crates/table/src/selection.rs | 7 ++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/app/src/status_items/mod.rs b/crates/app/src/status_items/mod.rs index d1e818a8..bf113449 100644 --- a/crates/app/src/status_items/mod.rs +++ b/crates/app/src/status_items/mod.rs @@ -45,7 +45,7 @@ pub fn build_status_bar_registry(cx: &mut App, dock: WeakEntity) -> St // Text readout of the table's selected cell. Added to the right *before* the agent star so it // lands leftmost in the right group (text items go right-leftmost, star stays rightmost). - let cell_location = cx.new(|cx| CellLocation::new(cx)); + let cell_location = cx.new(CellLocation::new); registry.add_right(cell_location); let agent = cx.new(|_| { diff --git a/crates/table/src/panel.rs b/crates/table/src/panel.rs index 22bfc3de..87fa4226 100644 --- a/crates/table/src/panel.rs +++ b/crates/table/src/panel.rs @@ -20,9 +20,8 @@ pub struct TablePanel { impl TablePanel { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let editor = cx.new(|cx| InputState::new(window, cx)); - let state = cx.new(|cx| { - TableState::new(QrateTableDelegate::new(editor.clone()), window, cx) - }); + let state = + cx.new(|cx| TableState::new(QrateTableDelegate::new(editor.clone()), window, cx)); // Publish the handle so status-bar items in the `app` crate can drive/read the table. cx.set_global(TableStateHandle(state.downgrade())); diff --git a/crates/table/src/selection.rs b/crates/table/src/selection.rs index 299ffb71..de32a9f7 100644 --- a/crates/table/src/selection.rs +++ b/crates/table/src/selection.rs @@ -24,7 +24,12 @@ pub(crate) fn render_cell( _window: &mut Window, cx: &mut Context>, ) -> AnyElement { - if delegate.editing == (EditState::Editing { row: row_ix, col: col_ix }) { + if delegate.editing + == (EditState::Editing { + row: row_ix, + col: col_ix, + }) + { return Input::new(&delegate.editor).into_any_element(); } From 6fb5abc1fa8382e6c493b6bc3f12483246d7c51f Mon Sep 17 00:00:00 2001 From: devnull03 <56480041+devnull03@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:03:58 +0530 Subject: [PATCH 5/5] debug(app): add title bars to debug scaffolding windows --- crates/app/src/debug.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/crates/app/src/debug.rs b/crates/app/src/debug.rs index b3578132..2aa75350 100644 --- a/crates/app/src/debug.rs +++ b/crates/app/src/debug.rs @@ -7,7 +7,7 @@ //! once. use gpui::*; -use gpui_component::{Root, TitleBar, button::Button, v_flex}; +use gpui_component::{Root, StyledExt, TitleBar, button::Button, label::Label, v_flex}; use settings::AppSettings; use window_wrapper::WindowRegistry; @@ -64,9 +64,14 @@ impl Render for DebugProjectStub { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .items_center() - .justify_center() - .child("Project Creation (debug stub) — ASNT-15 not built yet") + .child(TitleBar::new().child(Label::new("Project Creation (debug)").font_semibold())) + .child( + v_flex() + .flex_1() + .items_center() + .justify_center() + .child("Project Creation (debug stub) — ASNT-15 not built yet"), + ) } } @@ -117,17 +122,22 @@ impl Render for DebugMockProjectWindow { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .items_center() - .justify_center() - .gap_4() - .child("Debug: mock project management window") + .child(TitleBar::new().child(Label::new("Project Management (debug)").font_semibold())) .child( - Button::new("open-main-project") - .label("Open Main Project") - .on_click(|_, window, cx| { - open_main_window(cx); - window.remove_window(); - }), + v_flex() + .flex_1() + .items_center() + .justify_center() + .gap_4() + .child("Debug: mock project management window") + .child( + Button::new("open-main-project") + .label("Open Main Project") + .on_click(|_, window, cx| { + open_main_window(cx); + window.remove_window(); + }), + ), ) } }