Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/app/src/app_menus.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use gpui::*;

use crate::debug::DebugOpenBoth; // DEBUG (ASNT-13 manual test scaffolding)

actions!(nav, [OpenSettings, Quit]);

pub fn app_menus() -> Vec<Menu> {
vec![Menu {
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,
Comment on lines +3 to 14
MenuItem::action("Quit", Quit),
],
Expand Down
152 changes: 152 additions & 0 deletions crates/app/src/debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! 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, StyledExt, TitleBar, button::Button, label::Label, v_flex};
use settings::AppSettings;
use window_wrapper::WindowRegistry;

// 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]);

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| MainWindow::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<Self>) -> impl IntoElement {
v_flex()
.size_full()
.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"),
)
}
}

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<Self>) -> impl IntoElement {
v_flex()
.size_full()
.child(TitleBar::new().child(Label::new("Project Management (debug)").font_semibold()))
.child(
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();
}),
),
)
}
}

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()
}
}
108 changes: 59 additions & 49 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,68 @@ 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;

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::{
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,
};
Comment on lines 22 to 29
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`).
// `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;
}
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<Workspace>,
status_bar: Entity<StatusBar>,
Expand Down Expand Up @@ -115,45 +155,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(SettingsWindowHandle::default());

cx.on_action(|_: &OpenSettings, cx| {
let state = cx.global::<SettingsWindowHandle>();

if let Some(handle) = &state.handle {
if handle.update(cx, |_, _, _| {}).is_ok() {
return;
} else {
cx.global_mut::<SettingsWindowHandle>().handle = None;
}
}
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| {
cx.global_mut::<SettingsWindowHandle>().handle = Some(window_handle.into());
})
.ok();
}
})
.detach();
});
cx.set_global(WindowRegistry::default());

cx.on_action(|_: &OpenSettings, cx| open_settings_window(cx));
// ----------------------------------------------

cx.set_menus(app_menus());
Expand All @@ -169,19 +176,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);
Comment on lines +180 to +191
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");
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/status_items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn build_status_bar_registry(cx: &mut App, dock: WeakEntity<DockArea>) -> 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(|_| {
Expand Down
9 changes: 0 additions & 9 deletions crates/settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,15 +425,6 @@ impl AppSettings {
}
}

// --- Settings Window State ---

#[derive(Clone, Default)]
pub struct SettingsWindowHandle {
pub handle: Option<AnyWindowHandle>,
}

impl Global for SettingsWindowHandle {}

// --- Settings Window ---

pub struct SettingsWindow {
Expand Down
5 changes: 2 additions & 3 deletions crates/table/src/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ pub struct TablePanel {
impl TablePanel {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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()));

Expand Down
7 changes: 6 additions & 1 deletion crates/table/src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ pub(crate) fn render_cell(
_window: &mut Window,
cx: &mut Context<TableState<QrateTableDelegate>>,
) -> 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();
}
Comment on lines +27 to 34

Expand Down
2 changes: 2 additions & 0 deletions crates/window-wrapper/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading
Loading