Skip to content

Rewrite HwndTerminal to build on ControlInteractivity and -Core - #20812

Open
Dustin L. Howett (DHowett) wants to merge 9 commits into
mainfrom
dev/duhowett/win7-wpf-termcontrol-squash
Open

Dustin L. Howett (DHowett) wants to merge 9 commits into
mainfrom
dev/duhowett/win7-wpf-termcontrol-squash

Conversation

@DHowett

@DHowett Dustin L. Howett (DHowett) commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

This pull request replaces all of HwndTerminal, which used to be a thin layer on top of TerminalCore.lib, with a new implementation that relies on the Interactivity and Core layers in TerminalControl.lib.

We used to spend a lot of time reimplementing interactivity components for HwndTerminal or--if I'm being honest--letting it rot and never giving it the improvements the rest of the application got.

Among those improvements:

  • Automatic scrolling finally works (as of Move AutoScroll down into Interactivity with a render timer #20710)
  • Selection works the same way as it does in Terminal proper (starts on the half-cell, uses an exclusive range)
  • Consistent double- and triple-click behavior without having to reimplement word- and line-wise expansion
  • Scroll wheel behavior
  • Shift+Click to suppress VT mouse mode
  • Ctrl+Scroll zoom
  • Scrolling and focus handling move down into the Win32 message loop from WPF

This pull request seeks to get HwndTerminal back to where it was (plus the things we naturally get from switching over). In the future, we can more easily add:

  • title reporting (surprised we didn't have this)
  • clickable URLs
  • automatic copy-to-clipboard
  • right-click menu support
  • DTTERM window manipulation
  • shaders

Implementation Details

The WPF control is backed by a flat "C" API, which takes in a void* and does some stuff to it. We call that a PTERM. References to "Flat C" in this commit body describe the body of work below WPF.

X-macros are used to fill out the definintions of every C API and bridge it to a properly typed C++ member function. Ugh. No more crap where we split the implementation into half C export and half C++ member.

ControlCore and ControlInteractivity require something that looks like TerminalSettings. The WPF control only supports the most basic settings, in struct TerminalTheme, and so I've added a bridge WinRT type to adapt TerminalTheme to
IXxxSettings. Most of the values it returns are our "defaults", and some of them are overridden.

Core requires an ITerminalConnection, but the WPF API uses a pair of "write output" and "send input" callbacks. Flat C provides an ITerminalConnection that bridges those callbacks. It does not communicate connection state or anything
else. Resizing is probably going to be a problem.

Core requires a dispatcher queue to ensure that some operations happen on the UI thread. I have stubbed out an implementation of IDispatcherQueue that calls a couple function pointers with a context pointer; the WPF code hooks up to that
and bridges it into the WPF dispatcher. Yes, the Core's dispatcher queue is the WPF main UI thread. It has to be, otherwise we'll explode the first time we try to update the scroll bar. Prep work was done in #20713.

To make the above easier, I removed all use of DispatcherTimer (see #20711). Attempts to use it will almost certainly cause the WPF control to implode.

Core needed to be taught a couple tricks to start up targeting an HWND instead of a composition handle. See #20722 for more.

Refs #20710
Refs #20711
Refs #20713
Refs #20722

Required #20721
Refs #20745

Related to #6999

@DHowett
Dustin L. Howett (DHowett) added this pull request to stack #20813 October 16, 2026 20:01

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check-spelling found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend a...

git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space (git merge-base origin/dev/duhowett/win7-wpf-termcontrol-squash origin/main) origin/dev/duhowett/win7-wpf-termcontrol-squash

it'll show you what is copied (gray) and what has been rewritten (red, green)

I wonder if I should rename FlatC to HwndTerminal and really make a mess of the diff.

Comment thread src/cascadia/WpfTerminalControl/TerminalContainer.cs Outdated
<RootNamespace>Microsoft.Terminal.Wpf</RootNamespace>
<AssemblyName>Microsoft.Terminal.Wpf</AssemblyName>
<UseWpf>true</UseWpf>
<LangVersion>9.0</LangVersion>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I used some fancy operators

#undef GENERATOR_N
#undef API_NAME

__declspec(dllexport) HRESULT __stdcall CreateTerminal(HWND parentHwnd, TerminalDispatcherCallouts dispatcherCallouts, _Out_ void** hwnd, _Out_ PTERM* terminal);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS FILE IS NOT INTENDED FOR PUBLICATION AND IS AN IMPLEMENTATION DETAIL FOR NOW

The idea is that if we DO add a flat C API, we could generate it using these macros. This just makes my life easier.

Comment thread src/cascadia/TerminalControl/HwndTerminal.cpp
Comment thread src/cascadia/TerminalControl/HwndTerminal.cpp
@DHowett
Dustin L. Howett (DHowett) force-pushed the dev/duhowett/win7-wpf-termcontrol-squash branch 3 times, most recently from dcf463d to 4f3012e Compare October 16, 2026 20:29
@github-actions

This comment has been minimized.

@DHowett
Dustin L. Howett (DHowett) force-pushed the dev/duhowett/win7-wpf-termcontrol-squash branch from 4f3012e to 8f9cd1e Compare October 16, 2026 20:34
@DHowett
Dustin L. Howett (DHowett) removed this pull request from stack #20813 October 19, 2026 15:25
Base automatically changed from dev/duhowett/scoped-no-resources to main October 19, 2026 16:32
@DHowett
Dustin L. Howett (DHowett) force-pushed the dev/duhowett/win7-wpf-termcontrol-squash branch from 8f9cd1e to 0591c34 Compare October 19, 2026 16:34
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
Comment thread src/cascadia/TerminalControl/FlatC.cpp Fixed
@github-actions

This comment has been minimized.

@DHowett Dustin L. Howett (DHowett) changed the title WIP: Replace HwndTerminal with HwndTerminal v2 (harder better faster stronger) Rewrite HwndTerminal to build on ControlInteractivity and -Core Oct 19, 2026
This pull request replaces all of HwndTerminal, which used to be a thin
layer on top of TerminalCore.lib, with a new implementation that relies
on the Interactivity and Core layers in TerminalControl.lib.

We used to spend a lot of time reimplementing interactivity components
for HwndTerminal or--if I'm being honest--letting it rot and never
giving it the improvements the rest of the application got.

Among those improvements:

- Automatic scrolling finally works (as of #20710)
- Selection works the same way as it does in Terminal proper (starts on
  the half-cell, uses an exclusive range)
- Consistent double- and triple-click behavior without having to
  reimplement word- and line-wise expansion
- Scroll wheel behavior
- Shift+Click to suppress VT mouse mode
- Ctrl+Scroll zoom

This pull request seeks to get HwndTerminal back to where it was (plus
the things we naturally get from switching over). In the future, we can
more easily add:

- title reporting (surprised we didn't have this)
- clickable URLs
- automatic copy-to-clipboard
- right-click menu support
- DTTERM window manipulation
- shaders

Implementation Details
----------------------

The WPF control is backed by a flat "C" API, which takes in a `void*`
and does some stuff to it. We call that a `PTERM`. References to "Flat
C" in this commit body describe the body of work below WPF.

X-macros are used to fill out the definintions of every C API and bridge
it to a properly typed C++ member function. Ugh. No more crap where we
split the implementation into half C export and half C++ member.

ControlCore and ControlInteractivity require something that looks like
TerminalSettings. The WPF control only supports the most basic settings,
in `struct TerminalTheme`, and so I've added a bridge WinRT type to
adapt TerminalTheme to IXxxSettings. Most of the values it returns are
our "defaults", and some of them are overridden.

Core requires an `ITerminalConnection`, but the WPF API uses a pair of
"write output" and "send input" callbacks. Flat C provides an
ITerminalConnection that bridges those callbacks. It does not
communicate connection state or anything else. Resizing is probably
going to be a problem.

Core requires a dispatcher queue to ensure that some operations happen
on the UI thread. I have stubbed out an implementation of
`IDispatcherQueue` that calls a couple function pointers with a context
pointer; the WPF code hooks up to that and bridges it into the WPF
dispatcher. Yes, the Core's dispatcher queue _is_ the WPF main UI
thread. It has to be, otherwise we'll explode the first time we try to
update the scroll bar. Prep work was done in #20713.

To make the above easier, I removed all use of DispatcherTimer
(see #20711). Attempts to use it will almost certainly cause the WPF
control to implode.

Core needed to be taught a couple tricks to start up targeting an HWND
instead of a composition handle. See #20722 for more.

Refs #20710
Refs #20711
Refs #20713
Refs #20722

Related to #6999
@DHowett
Dustin L. Howett (DHowett) force-pushed the dev/duhowett/win7-wpf-termcontrol-squash branch from 3bdc1e8 to 86b388c Compare October 19, 2026 22:05
@DHowett

Copy link
Copy Markdown
Member Author

/cc Javier (@javierdlg) it is finally here

@lhecker Leonard Hecker (lhecker) left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing literally lags by up to 5s while in the diff view.

GitHub, this is the most awful experience imaginable and it's been going even further downhill for years.

If you were to make a graph of website performance, it would be a vertical line through the floor. That takes some special talent.

Comment on lines +2715 to +2716
auto scrollbarSize = GetSystemMetrics(SM_CXVSCROLL);
scrollbarSize = gsl::narrow_cast<decltype(scrollbarSize)>(scrollbarSize * (dpi / 96));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh?

And also: scrollbarSize * (dpi / 96) = integer scale only.

Comment on lines +12 to +13
TerminalTriggerResize
TerminalTriggerResizeWithDimension

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCHOCKSCHWERENOT! AN UNSORTED LIST!

Comment on lines +17 to +18
typedef void(__stdcall* PSCROLLCB)(int, int, int);
typedef void(__stdcall* PWRITECB)(const wchar_t*);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird names with no param names?

BTW C# does not support strings with lengths huh? (For PWRITECB.)

Comment on lines +22 to +23
bool(__stdcall* pDispatcherTryEnqueueWork)(void*, int, void*);
bool(__stdcall* pDispatcherHasThreadAccess)(void*);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why p prefix? If anything, it should be fn or pfn, right?

} TerminalDispatcherCallouts;

#define TERMINAL_API_TABLE(XX) \
XX(SendOutput, LPCWSTR, data) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot discern an order of these functions, but I think it could be sorted.

0, // Mouse
MouseButtonStateFromWParam(wParam),
uMsg,
std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use std::chrono::microseconds throughout ControlCore for timings? What the heck, that completely went by me!

And why system_clock? It should be steady_clock I'm pretty sure. And we should pass std::chrono::steady_clock::time_point or we migrate over to double and seconds, which would be good for the next 2000 years.

0,
term_window_class,
nullptr,
WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why all these flags? We're a swap chain so we aren't subject to GDI in the first place, aren't we?

wil::assign_to_opt_param(dimensions, til::size{
static_cast<til::CoordType>(width / fontSizeInPx.Width),
static_cast<til::CoordType>(height / fontSizeInPx.Height),
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Odd indentation.

Comment on lines +451 to +453
auto concatenated = std::accumulate(std::begin(strings), std::end(strings), std::wstring{}, [](auto&& l, auto&& r) {
return l + r;
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an odd way to copy a hstring into a wstring. :D
It's also not needed, because make_cotaskmem_string_nothrow takes a hstring.c_str() anyway.

Comment on lines +596 to +597
bool _initialized{ false };
bool _focused{ false };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunate struct order?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants