Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a74bc83
Add Pane tree + SplitPanel scaffolding for split-pane support
i999rri May 19, 2026
b8bcca3
Route Tab content through the Pane tree / SplitPanel
i999rri May 19, 2026
05fd349
Move surface identification from Tab to leaf-level PaneId
i999rri May 19, 2026
0844c49
Add tree-mutation primitives for in-place pane edits
i999rri May 19, 2026
3ac19de
Handle GHOSTTY_ACTION_NEW_SPLIT
i999rri May 19, 2026
2941377
Collapse split when a pane in a multi-pane tab closes
i999rri May 19, 2026
731fbdc
Draggable splitter strip between sibling panes
i999rri May 20, 2026
726b667
Handle GHOSTTY_ACTION_RESIZE_SPLIT
i999rri May 20, 2026
2266cd6
Handle GHOSTTY_ACTION_GOTO_SPLIT
i999rri May 20, 2026
b9e3df1
Show focus border around the active pane
i999rri May 20, 2026
8d83103
Handle GHOSTTY_ACTION_EQUALIZE_SPLITS
i999rri May 20, 2026
a95d2bd
Handle GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM
i999rri May 20, 2026
578e5a3
SEH-guard the new-split surface creation
i999rri May 20, 2026
0957c1e
Drop splitter cursor — Border.ProtectedCursor isn't accessible
i999rri May 20, 2026
27daf4a
Use get_strong().as<UIElement>() for GetCurrentPoint relativeTo
i999rri May 20, 2026
d4190ad
Define NOMINMAX + compile with /utf-8
i999rri May 20, 2026
28cc9b8
Revert /utf-8 flag — broke PCH compilation
i999rri May 20, 2026
e4c5140
Reset pane Visibility on every tree rebuild
i999rri May 20, 2026
7763442
Defer focus restore on window activation past XAML default-focus
i999rri May 20, 2026
3e25ae3
Add diagnostic logging for activation focus + split actions
i999rri May 20, 2026
aac0ea3
Log focus + drag-region pointer events for title-bar repro
i999rri May 20, 2026
cb50b30
Re-activate window after title-bar drag-region click
i999rri May 20, 2026
f5756b5
Recover spurious deactivation regardless of click path
i999rri May 20, 2026
a0bea82
Defer the spurious-deactivation foreground check
i999rri May 20, 2026
79adefd
Restore focus directly from DragRegion PointerReleased
i999rri May 20, 2026
bb45bda
Remove diagnostic logging used to chase the title-bar focus bug
i999rri May 20, 2026
469ba6e
Auto-hide focus border 1.5s after the latest focus change
i999rri May 20, 2026
7107377
Pane chrome: 2 DIP splitter line, no outer focus border
i999rri May 20, 2026
3679ee2
Bring back the 1.5 s focus-border flash as an overlay
i999rri May 20, 2026
003f168
Extract focus-border colours + hide delay into named constants
i999rri May 20, 2026
b874ee6
RESIZE_SPLIT: arrow direction == boundary direction
i999rri May 20, 2026
cce29c5
Track active surface via TerminalControl focus events
i999rri May 23, 2026
20ebb9a
Replace focus border with upstream-style dim overlay
i999rri May 23, 2026
34ee688
Halve splitter thickness to 1 DIP
i999rri May 23, 2026
f969f29
Drive splitter color from ghostty split-divider-color config
i999rri May 23, 2026
fc6f214
ghostty_config_get: pass key length, not output buffer size
i999rri May 23, 2026
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
5 changes: 5 additions & 0 deletions GhosttyWin32App/GhosttyApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class GhosttyApp {
}

ghostty_app_t Handle() const noexcept { return m_app; }
// Borrow the parsed config. Callers use ghostty_config_get against
// this handle for read-only lookups (e.g. unfocused-split-opacity);
// the GhosttyApp keeps ownership and frees it in the destructor
// after the app handle is released.
ghostty_config_t ConfigHandle() const noexcept { return m_config; }
void Tick() noexcept { if (m_app) ghostty_app_tick(m_app); }

private:
Expand Down
10 changes: 8 additions & 2 deletions GhosttyWin32App/GhosttyWin32App.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@
<ClInclude Include="MainWindow.xaml.h">
<DependentUpon>MainWindow.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="Pane.h" />
<ClInclude Include="PaneId.h" />
<ClInclude Include="PaneIdAllocator.h" />
<ClInclude Include="SplitPanel.h" />
<ClInclude Include="Tab.h" />
<ClInclude Include="TabFactory.h" />
<ClInclude Include="TabId.h" />
<ClInclude Include="TabIdAllocator.h" />
<ClInclude Include="TerminalControl.xaml.h">
<DependentUpon>TerminalControl.xaml</DependentUpon>
</ClInclude>
Expand All @@ -154,6 +156,7 @@
<DependentUpon>MainWindow.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="SEHGuard.cpp" />
<ClCompile Include="SplitPanel.cpp" />
<ClCompile Include="TerminalControl.xaml.cpp">
<DependentUpon>TerminalControl.xaml</DependentUpon>
</ClCompile>
Expand All @@ -164,6 +167,9 @@
<SubType>Code</SubType>
<DependentUpon>MainWindow.xaml</DependentUpon>
</Midl>
<Midl Include="SplitPanel.idl">
<SubType>Code</SubType>
</Midl>
<Midl Include="TerminalControl.idl">
<SubType>Code</SubType>
<DependentUpon>TerminalControl.xaml</DependentUpon>
Expand Down
711 changes: 666 additions & 45 deletions GhosttyWin32App/MainWindow.xaml.cpp

Large diffs are not rendered by default.

61 changes: 59 additions & 2 deletions GhosttyWin32App/MainWindow.xaml.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
#include "MainWindow.g.h"
#include "ghostty.h"
#include "GhosttyApp.h"
#include "PaneIdAllocator.h"
#include "Tab.h"
#include "TabFactory.h"
#include "TabIdAllocator.h"
#include "Tabs.h"

namespace winrt::GhosttyWin32::implementation
Expand Down Expand Up @@ -33,6 +33,22 @@ namespace winrt::GhosttyWin32::implementation
void OnCloseClick(winrt::Windows::Foundation::IInspectable const&,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const&);

// Called by every TerminalControl when it receives keyboard
// focus (wired by TabFactory). Updates m_activeSurface so any
// future caller — APP-target action handlers, multi-window
// focus delivery, IPC / scripting bridges — can ask "which
// surface is the user looking at right now" without a
// separate tree walk. Public because TerminalControl needs to
// reach in via the host-supplied callback. UI thread only.
void NotifySurfaceFocused(ghostty_surface_t surface) noexcept;

// Last surface to receive keyboard focus inside this window.
// Null until the first focus delivery (typically the very
// first tab's TerminalControl GotFocus right after launch).
// Stays valid across alt-tab — we only clear it when the
// surface itself is torn down.
ghostty_surface_t GetActiveSurface() const noexcept { return m_activeSurface; }

private:
void InitGhostty();
void CreateTab();
Expand All @@ -45,10 +61,51 @@ namespace winrt::GhosttyWin32::implementation
// depending on the current OverlappedPresenter state.
void UpdateMaximizeGlyph();

// Handle GHOSTTY_ACTION_NEW_SPLIT: locate the source pane for
// `surface`, create a new TerminalControl + ghostty surface,
// and insert it next to the source according to `direction`.
// The new pane becomes the active leaf and takes focus. UI
// thread only.
void SplitActivePane(ghostty_surface_t surface,
ghostty_action_split_direction_e direction);

// Tear down the pane carrying `id` and update the tree / tab
// list. Dispatched from close_surface_cb. UI thread only.
void CloseSurfaceByPaneId(PaneId id);

// Handle GHOSTTY_ACTION_RESIZE_SPLIT: walk up from the active
// pane to the nearest ancestor split whose axis matches the
// direction, then nudge that split's ratio by `amount` DIPs
// in the requested direction. UI thread only.
void ResizeSplitFromAction(ghostty_surface_t surface,
ghostty_action_resize_split_s resize);

// Handle GHOSTTY_ACTION_GOTO_SPLIT: move focus to another
// pane in the same tab. PREVIOUS/NEXT cycle the tree in
// depth-first order; UP/DOWN/LEFT/RIGHT pick the leaf whose
// arranged rect is adjacent in that direction. UI thread only.
void GotoSplitFromAction(ghostty_surface_t surface,
ghostty_action_goto_split_e direction);

// Handle GHOSTTY_ACTION_EQUALIZE_SPLITS: reset every split
// ratio in the active tab to 0.5 so each pane occupies an
// even share of its parent split. UI thread only.
void EqualizeSplitsForSurface(ghostty_surface_t surface);

// Handle GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM: if no leaf is
// currently zoomed in the source surface's tab, expand the
// source leaf to fill the panel; if a leaf is already zoomed
// there, restore the regular split layout. UI thread only.
void ToggleSplitZoomForSurface(ghostty_surface_t surface);

std::unique_ptr<GhosttyApp> m_ghostty;
HWND m_hwnd = nullptr;
TabIdAllocator m_tabIds;
PaneIdAllocator m_paneIds;
Tabs m_tabs;
// Focus-tracked active surface. Set by NotifySurfaceFocused
// when a TerminalControl gains focus, cleared when the
// matching surface is torn down through CloseSurfaceByPaneId.
ghostty_surface_t m_activeSurface = nullptr;
// Constructed once ghostty is initialized — needs the app handle
// and HWND, neither available until InitGhostty has run.
std::unique_ptr<TabFactory> m_tabFactory;
Expand Down
190 changes: 190 additions & 0 deletions GhosttyWin32App/Pane.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#pragma once

#include "PaneId.h"
#include <winrt/Microsoft.UI.Xaml.h>
#include <winrt/Windows.Foundation.h>
#include <memory>

namespace winrt::GhosttyWin32::implementation {

// Direction of a split node.
//
// Horizontal : children laid out side by side, the split bar is vertical.
// Vertical : children stacked, the split bar is horizontal.
enum class SplitOrientation {
Horizontal,
Vertical,
};

// Binary-tree node describing how a Tab's content is partitioned into
// terminal panes. A node is either a leaf (one UI element fills the
// node's rectangle) or an internal node (two children, split by
// `orientation` at `ratio`).
//
// Leaves hold a plain `UIElement` rather than a `TerminalControl` so
// the same tree can host placeholder Borders during layout-only tests
// and real TerminalControls in production. SplitPanel only cares about
// "something to arrange", so erasing to the base class also keeps that
// renderer agnostic.
//
// Ownership is unique_ptr-based: a parent owns its two children, the
// SplitPanel owns the root. Parent back-pointers (`m_parent`) are kept
// so direction-based navigation can walk up and across without a
// separate index; they're set when a node is attached as a child and
// cleared on detach.
//
// The class is move-disabled because raw `Pane*` back-pointers must
// stay valid for the node's lifetime — moving would invalidate every
// child's `m_parent`.
class Pane {
public:
// Construct a leaf wrapping `content`. `content` is the actual
// UIElement that will be added to the SplitPanel's Children
// collection when the leaf is arranged. `id` identifies the leaf
// for ghostty's close_surface_cb routing — required to be non-zero
// for production callers; a default-constructed PaneId is accepted
// (and exposed as Id()) so layout-only tests can build trees
// without an allocator.
static std::unique_ptr<Pane> MakeLeaf(Microsoft::UI::Xaml::UIElement content,
PaneId id = {}) {
auto p = std::unique_ptr<Pane>(new Pane{});
p->m_content = std::move(content);
p->m_id = id;
return p;
}

// Construct an internal node by combining two existing subtrees.
// `ratio` is the fraction of the available extent the first child
// gets along the split axis (clamped to [0.05, 0.95] to keep both
// children meaningfully visible).
static std::unique_ptr<Pane> MakeSplit(
SplitOrientation orientation,
double ratio,
std::unique_ptr<Pane> first,
std::unique_ptr<Pane> second
) {
auto p = std::unique_ptr<Pane>(new Pane{});
p->m_orientation = orientation;
p->m_ratio = ClampRatio(ratio);
p->m_first = std::move(first);
p->m_second = std::move(second);
if (p->m_first) p->m_first->m_parent = p.get();
if (p->m_second) p->m_second->m_parent = p.get();
return p;
}

~Pane() = default;
Pane(const Pane&) = delete;
Pane& operator=(const Pane&) = delete;
Pane(Pane&&) = delete;
Pane& operator=(Pane&&) = delete;

bool IsLeaf() const noexcept { return static_cast<bool>(m_content); }

// Leaf accessor. Returns null for internal nodes.
Microsoft::UI::Xaml::UIElement Content() const noexcept { return m_content; }

// Leaf-side identifier set at MakeLeaf time. Used as cfg.userdata
// for ghostty's close_surface_cb so the host can route the
// callback back to a specific leaf. Internal nodes have no ID
// (returns the zero sentinel).
PaneId Id() const noexcept { return m_id; }

// Internal-node accessors. Calling these on a leaf returns
// default-initialized values / nullptrs — callers should gate on
// IsLeaf() first.
SplitOrientation Orientation() const noexcept { return m_orientation; }
double Ratio() const noexcept { return m_ratio; }
void SetRatio(double ratio) noexcept { m_ratio = ClampRatio(ratio); }
Pane* First() const noexcept { return m_first.get(); }
Pane* Second() const noexcept { return m_second.get(); }

// Back-pointer to the enclosing internal node, or null at the root.
Pane* Parent() const noexcept { return m_parent; }

// Most-recent rectangle this node was arranged into, in
// SplitPanel-local coordinates. Set by SplitPanel::ArrangeNode on
// every layout pass; consumed by splitter-drag resize math and
// direction-based GOTO_SPLIT (finding the leaf adjacent to the
// active one). Defaults to zero before the first arrange, which
// callers treat as "no info" — the host should defer any rect-
// dependent action until the user can see the panel anyway.
Windows::Foundation::Rect ArrangedRect() const noexcept { return m_arrangedRect; }
void SetArrangedRect(Windows::Foundation::Rect r) noexcept { m_arrangedRect = r; }

// Swap the child unique_ptr matching `oldChild` for `newChild`.
// Returns true when `oldChild` was one of this node's children
// (the old subtree is destroyed when its unique_ptr is overwritten;
// the new child's parent back-pointer is rewritten to point here).
// No-op + false if called on a leaf or if `oldChild` isn't a
// child — the caller is expected to verify membership when the
// distinction matters.
//
// Used by SplitPanel::ReplaceLeaf for in-place tree edits like
// NEW_SPLIT (replace a leaf with a split subtree wrapping it) and
// CLOSE_PANE (replace a split with its surviving child).
bool ReplaceChild(Pane* oldChild, std::unique_ptr<Pane> newChild) noexcept {
if (IsLeaf() || !oldChild || !newChild) return false;
if (m_first.get() == oldChild) {
newChild->m_parent = this;
m_first = std::move(newChild);
return true;
}
if (m_second.get() == oldChild) {
newChild->m_parent = this;
m_second = std::move(newChild);
return true;
}
return false;
}

// Pull `child` out of this internal node and return ownership.
// The child becomes an orphan (parent back-pointer cleared) so
// the caller can re-attach it elsewhere — typically as a
// replacement for this node itself when collapsing a split.
// Returns nullptr if called on a leaf or if `child` isn't a child.
std::unique_ptr<Pane> DetachChild(Pane* child) noexcept {
if (IsLeaf() || !child) return nullptr;
if (m_first.get() == child) {
m_first->m_parent = nullptr;
return std::move(m_first);
}
if (m_second.get() == child) {
m_second->m_parent = nullptr;
return std::move(m_second);
}
return nullptr;
}

private:
Pane() = default;

static double ClampRatio(double r) noexcept {
if (r < 0.05) return 0.05;
if (r > 0.95) return 0.95;
return r;
}

// Leaf payload — non-null iff IsLeaf().
Microsoft::UI::Xaml::UIElement m_content{ nullptr };

// Leaf identifier — zero sentinel for internal nodes and for
// layout-only test leaves built without an allocator.
PaneId m_id{};

// Internal-node fields — unused when IsLeaf().
SplitOrientation m_orientation{ SplitOrientation::Horizontal };
double m_ratio{ 0.5 };
std::unique_ptr<Pane> m_first;
std::unique_ptr<Pane> m_second;

// Set by MakeSplit when this node is attached as a child; null at
// the root. Used by future phases for direction-based navigation
// and for collapsing the tree on pane close.
Pane* m_parent{ nullptr };

// Refreshed on every SplitPanel arrange pass — see ArrangedRect.
Windows::Foundation::Rect m_arrangedRect{};
};

} // namespace winrt::GhosttyWin32::implementation
20 changes: 13 additions & 7 deletions GhosttyWin32App/TabId.h → GhosttyWin32App/PaneId.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@

namespace winrt::GhosttyWin32::implementation {

// Strongly-typed tab identifier. Wraps a uint64_t so that
// Strongly-typed pane identifier. Wraps a uint64_t so that
// - random uint64_t values (DPI, sizes, counts) can't be mistakenly
// passed as a TabId
// passed as a PaneId
// - the void* boundary with ghostty's cfg.userdata is centralized in
// ToUserdata / FromUserdata, instead of bare reinterpret_casts
// scattered through the host
//
// Allocated once per leaf in TabFactory and threaded through
// cfg.userdata so close_surface_cb can route back to the right pane.
// Today every tab has exactly one pane so a PaneId 1:1 maps to a Tab,
// but once splits land each leaf gets its own ID and close_surface_cb
// can identify the specific pane without ambiguity.
//
// Value 0 is reserved as a sentinel meaning "no ID" (default-constructed
// TabId converts to false).
struct TabId {
// PaneId converts to false).
struct PaneId {
uint64_t value{ 0 };

constexpr bool operator==(TabId const&) const noexcept = default;
constexpr bool operator==(PaneId const&) const noexcept = default;
explicit constexpr operator bool() const noexcept { return value != 0; }

// void* boundary helpers. cfg.userdata is opaque to ghostty — it
Expand All @@ -25,8 +31,8 @@ struct TabId {
void* ToUserdata() const noexcept {
return reinterpret_cast<void*>(static_cast<uintptr_t>(value));
}
static TabId FromUserdata(void* p) noexcept {
return TabId{ static_cast<uint64_t>(reinterpret_cast<uintptr_t>(p)) };
static PaneId FromUserdata(void* p) noexcept {
return PaneId{ static_cast<uint64_t>(reinterpret_cast<uintptr_t>(p)) };
}
};

Expand Down
30 changes: 30 additions & 0 deletions GhosttyWin32App/PaneIdAllocator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include "PaneId.h"
#include <atomic>

namespace winrt::GhosttyWin32::implementation {

// Issues monotonically increasing PaneIds. One instance per MainWindow —
// the counter is per-allocator (not process-global) so that test setups
// or future multi-window scenarios get an isolated ID space without
// having to clear hidden static state.
//
// Move-disabled: fixed location for the lifetime of MainWindow.
class PaneIdAllocator {
public:
PaneIdAllocator() = default;
PaneIdAllocator(const PaneIdAllocator&) = delete;
PaneIdAllocator& operator=(const PaneIdAllocator&) = delete;
PaneIdAllocator(PaneIdAllocator&&) = delete;
PaneIdAllocator& operator=(PaneIdAllocator&&) = delete;

PaneId Allocate() noexcept {
return PaneId{ m_next.fetch_add(1, std::memory_order_relaxed) };
}

private:
std::atomic<uint64_t> m_next{ 1 }; // 0 reserved as sentinel by PaneId
};

} // namespace winrt::GhosttyWin32::implementation
Loading
Loading