Files
microsoft-ui-xaml/controls/dev/TableView/TableViewHeaderCell.h
T
866ba81a1c TableView: merge feature/TableView into main (#12267, #12256, #12248, #12120) (#12285)
Merges the TableView feature branch `user/hik/feature/TableView` into
`main`. Each change below was reviewed and merged into the feature
branch as its own PR. This PR brings them to `main` together.

## Included PRs
- **#12267 Hierarchical and live data operations (review fixes)**
(@dipeshmsft)
  - Group/tree collapse keeps selection consistent.
  - Live-shaping subscriptions are hardened.
  - Code-review and benchmark-app fixes.
- **#12256 [2/3] TableView: cell UIA names, stale peers, grow-only Auto
width, regroup selection, HC backplates, rounded focus**
  - Cell accessible names don't repeat the column header.
  - Stale GridItem peers throw ElementNotAvailable.
  - Group StructureChanged is gated on ListenerExists.
  - Auto columns only grow within a data set.
  - A regroup keeps the moved selected item selected.
  - No high-contrast backplates on hovered/pressed rows.
- Keyboard focus rects are rounded on cells, header cells and group
headers.
- **#12248 [3/3] TableView: ListView paging and far-row focus, header
naming/focus/gripper, source ownership, safe sort, edit rollback**
- **Paging and focus:** ListView-style PageUp/PageDown (group headers
are valid targets); far-row focus on Home/End and Ctrl+Home/End.
  - **Headers:** naming, focus restore and resize-gripper fixes.
- **Source ownership:** single-owner `TableViewSource` with
release/reclaim on Unloaded/Loaded.
  - **Sorting:** fails safely over null or duplicate rows.
  - **Editing:** rollback of invalid values on forced edit teardown.
- **Sample-app findings fixed:** a layout-cycle crash, fast-paging
focus, ComboBox editor focus, Ctrl+Space selection.

- **#12120 TableView: add usage telemetry and Test in Production**
(@Saharsh979)
- Reliability (TiP) and usage telemetry on the existing Tabular
providers. No public API change.
- Two TiP tests: `TableViewInitializationTest` runs on
`OnApplyTemplate`; `TableViewOperationTest` covers source swap, sort,
filter, grouping, selection and edit begin/commit/cancel.
- Usage telemetry events: `TableView_SessionSummary` and
`TableView_Error`.
- Build: `TableView.TestInProduction.props` is imported by Tabular only,
and `MuxcTraceLogging.h` gains `KEYWORD_TABLEVIEW`.
`main` has 3 NavigationView commits that aren't on the feature branch.
They don't touch TableView, and the two sides merge with no conflicts.

## Validation
On a test VM, using the feature-branch bits before the final #12248
rebase (before #12120 landed):
- **TableView API suite:** 581/582 passed.
- **TableView interaction suite:** 122/122 passed.
- **TableViewSampleApp:** sanity pass on every page (paging, grouping,
editing, sort, navigating away and back).

- **Build fix (32f8ff44a):** #12120 added a TiP generation argument to
`SyncTableViewSourceSort`, but the `RerankCustomSortAfterReclaim` call
from #12248 still passed two arguments. Git merged the two cleanly, so
the conflict only showed up as a C2660 build error. The rerank is driven
by the control, not the user, so it now passes `0` (no TiP operation).

## Known issues
- **Unnamed visual cells (8504536eb):** a cell that shows an unnamed
visual, such as a ProgressBar, is now named after its column instead of
"blank", so `VerifyUnnamedVisualCellIsNotNamedBlank` passes (3/3 on the
test VM).
- **3 localization tests are temporarily disabled** (in #12248).
- **Follow-ups from the #12248 review:**
  - PageUp/PageDown distance with variable-height rows.
- The identity-validation cache misses changes in collections that don't
raise change notifications.
  - Interaction coverage for header hover/press visuals.
- **Theme resources:** unused `TabularSurface*` theme keys and their
`MUX_TabularSurface*` aliases were removed. They only shipped in
experimental packages, so this is worth a release note.

---------

Co-authored-by: Dipesh Kumar <dipeshkumar@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Dipesh Kumar <85861525+dipeshmsft@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hiteshkrmsft <169995515+hiteshkrmsft@users.noreply.github.com>
Co-authored-by: Saharsh Ananta Jaiswal <sahjaiswal@microsoft.com>
Copilot-Session: 1a711a5a-8e3c-4114-a58b-68a5e3784da9
Copilot-Session: 1b4b389b-9df6-4996-bd1c-b4a30a5da768
Copilot-Session: d8e58903-f3be-45f2-8124-5cdcfcca0428
Copilot-Session: b54b3a5d-2e82-43fa-9664-760e896cc4a3
Copilot-Session: 1cda586e-f933-4841-9256-463216c2c118
Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
Copilot-Session: a4f445d6-e2e7-416c-a777-7bc28103da7e
2026-10-15 11:41:27 +05:30

211 lines
7.3 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
#pragma once
#include "TableViewColumnHeaderAutomationPeer.h"
// Keep the existing Grid layout/input behavior, but attach the header peer to the
// actual focus and hit-test target. This implementation-only type needs no WinRT API.
class TableViewHeaderCell :
public ReferenceTracker<TableViewHeaderCell, winrt::Microsoft::UI::Xaml::Controls::GridT, winrt::composable>
{
public:
TableViewHeaderCell(winrt::TableView const& table, winrt::TableViewColumn const& column)
: m_table(winrt::make_weak(table)), m_column(winrt::make_weak(column))
{
// A Grid has no pointer overrides. Never marks the events handled, so Tapped-to-sort and
// focus are unaffected.
PointerEntered({ this, &TableViewHeaderCell::OnPointerEntered });
PointerExited({ this, &TableViewHeaderCell::OnPointerExited });
PointerPressed({ this, &TableViewHeaderCell::OnPointerPressed });
PointerReleased({ this, &TableViewHeaderCell::OnPointerReleased });
PointerCaptureLost({ this, &TableViewHeaderCell::OnPointerLost });
PointerCanceled({ this, &TableViewHeaderCell::OnPointerLost });
// Theme brushes are resolved per state, so a theme change while hovered must re-resolve them.
ActualThemeChanged({ this, &TableViewHeaderCell::OnActualThemeChanged });
}
winrt::hstring GetRuntimeClassName() const
{
return winrt::hstring_name_of<winrt::Grid>();
}
winrt::AutomationPeer OnCreateAutomationPeer()
{
return winrt::make<TableViewColumnHeaderAutomationPeer>(
get_strong().as<winrt::FrameworkElement>(), m_table.get(), m_column.get());
}
private:
enum class PointerVisualState
{
Normal,
PointerOver,
Pressed,
};
void OnPointerEntered(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs&)
{
m_isPointerOver = true;
UpdatePointerVisualState();
}
void OnPointerExited(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs&)
{
m_isPointerOver = false;
m_isPressed = false;
UpdatePointerVisualState();
}
void OnPointerPressed(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs&)
{
m_isPressed = true;
UpdatePointerVisualState();
}
void OnPointerReleased(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs&)
{
m_isPressed = false;
UpdatePointerVisualState();
}
void OnPointerLost(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args)
{
// The resize gripper captures the pointer for its drag; that capture ending bubbles here
// while the pointer is still over the header, so it must not reset the hover state.
if (IsInsideResizeGripper(args.OriginalSource()))
{
return;
}
m_isPointerOver = false;
m_isPressed = false;
UpdatePointerVisualState();
}
void OnActualThemeChanged(const winrt::FrameworkElement&, const winrt::IInspectable&)
{
UpdatePointerVisualState(true /* force */);
}
bool IsInsideResizeGripper(const winrt::IInspectable& source)
{
winrt::Panel const self = *this;
for (auto current = source.try_as<winrt::DependencyObject>();
current && current != self;
current = winrt::VisualTreeHelper::GetParent(current))
{
if (current.try_as<winrt::ResizeGripper>())
{
return true;
}
}
return false;
}
bool IsSortable() const
{
auto const table = m_table.get();
auto const column = m_column.get();
return table && column && table.CanUserSortColumns() && column.CanSort();
}
void UpdatePointerVisualState(bool force = false)
{
auto state = PointerVisualState::Normal;
if (IsSortable())
{
state = m_isPressed ? PointerVisualState::Pressed
: m_isPointerOver ? PointerVisualState::PointerOver
: PointerVisualState::Normal;
}
if (state == m_visualState && !force)
{
return;
}
m_visualState = state;
winrt::ContentPresenter content{ nullptr };
if (Children().Size() > 0)
{
content = Children().GetAt(0).try_as<winrt::ContentPresenter>();
}
auto const indicator = TableView::FindSortIndicator(*this);
auto const table = m_table.get();
if (state == PointerVisualState::Normal || !table)
{
Background(winrt::SolidColorBrush{ winrt::Colors::Transparent() });
ApplyStateForeground(content, indicator, nullptr);
return;
}
auto const tableImpl = winrt::get_self<TableView>(table);
const bool pressed = state == PointerVisualState::Pressed;
// A null brush would make the cell stop taking pointer input.
winrt::Brush background = tableImpl->LookupHeaderStateBrush(pressed
? L"TabularSurfaceHeaderBackgroundPressedBrush"
: L"TabularSurfaceHeaderBackgroundPointerOverBrush");
if (!background)
{
background = winrt::SolidColorBrush{ winrt::Colors::Transparent() };
}
Background(background);
winrt::Brush foreground{ nullptr };
if (tableImpl->IsHighContrast())
{
foreground = tableImpl->LookupHeaderStateBrush(pressed
? L"TabularSurfaceHeaderForegroundPressedBrush"
: L"TabularSurfaceHeaderForegroundPointerOverBrush");
}
ApplyStateForeground(content, indicator, foreground);
}
// The chevron needs the same HC foreground as the text, or it vanishes on the highlight fill. The
// text backplate is turned off with it, as the row and group header templates do, so it does not
// fight the highlight fill.
static void ApplyStateForeground(
winrt::ContentPresenter const& content,
winrt::SortIndicator const& indicator,
winrt::Brush const& foreground)
{
if (content)
{
if (foreground)
{
content.Foreground(foreground);
content.HighContrastAdjustment(winrt::ElementHighContrastAdjustment::None);
}
else
{
content.ClearValue(winrt::ContentPresenter::ForegroundProperty());
content.ClearValue(winrt::UIElement::HighContrastAdjustmentProperty());
}
}
if (indicator)
{
if (foreground)
{
indicator.Foreground(foreground);
indicator.HighContrastAdjustment(winrt::ElementHighContrastAdjustment::None);
}
else
{
indicator.ClearValue(winrt::Control::ForegroundProperty());
indicator.ClearValue(winrt::UIElement::HighContrastAdjustmentProperty());
}
}
}
winrt::weak_ref<winrt::TableView> m_table;
winrt::weak_ref<winrt::TableViewColumn> m_column;
PointerVisualState m_visualState{ PointerVisualState::Normal };
bool m_isPointerOver{ false };
bool m_isPressed{ false };
};