mirror of
https://github.com/microsoft/microsoft-ui-xaml.git
synced 2026-10-28 16:35:19 +08:00
## Fixes | Internal issue | Type | Change | |---|---|---| | https://task.ms/64475625 | Bug | Column headers are a templated, restylable `TableViewColumnHeader` with visual states | | https://task.ms/64475623 | Bug | Header typography comes from the header Style, not C++ local values | | https://task.ms/64143451 | Bug | Header column separators are always drawn (no new property); hide them by overriding `TabularSurfaceHeaderSeparatorBrush` | | https://task.ms/63996584 | Task | Column headers expose the UIA Transform pattern so UIA clients can resize columns | | https://task.ms/64475611 | Bug | Outer border uses new `TabularSurfaceOuterBorderBrush` (`CardStrokeColorDefault`); grid-line values were in #12349 | | https://task.ms/64475629 | Bug | Default group header uses theme tokens (SemiBold key style, 8px spacing); the item count inherits the header `Foreground` | | https://task.ms/64652961 | Task | Row recycling no longer regenerates cells or refreshes row chrome twice (scroll performance) | | https://task.ms/63790855 | Task | Sort: `CanSort` is enforced and `SortByColumn(column, None)` always clears; a sort through a source another table owns returns false; no spurious `Sorted`; an app-owned sort survives a `CanSort`/`CanUserSortColumns` opt-out | | https://task.ms/63787021 | Task | Cell automation-peer constructor validates its arguments; `BeginEdit` rolls back when it throws | Not changed: https://task.ms/64512732 (by design: Auto columns follow content width), https://task.ms/64475627 (design decision: keep chevron sort glyphs), https://task.ms/64513223 (investigation only; fix deferred). ## PR Type - [x] Bugfix - [x] Feature - [ ] Code style update (formatting, renaming) - [ ] Refactoring (no functional changes, no API changes) - [ ] Build related changes - [x] Documentation content changes - [ ] Other (please describe): ## Description Follow-up to #12349 (merged) and #12285. Column headers move from code-built Grids to a new templated `TableViewColumnHeader`, restylable through `TableView.ColumnHeaderStyle` or `TableViewColumn.HeaderStyle`. Header peers gain the UIA Transform pattern for column resize. Sort behavior is tightened around `CanSort`, opt-outs and shared `TableViewSource` ownership. Row recycling reuses cells. Outer border, header lines and the default group header follow Fluent tokens. Review-driven fixes included here: - The header Style's `TargetType` `TypeName` is kept alive while it is compared; a Style with only `BasedOn` typed is applied, and its setters are read before applying it seals the Style. Incompatible, untyped or cyclic `BasedOn` Styles are ignored with a debug diagnostic. - A header disabled by its Style reports `IsEnabled=false` and is not invokable through UIA. - UIA `Transform.Resize` converts physical pixels through the table's full visual scale (not only `RasterizationScale`), and a width change below the drag jitter deadband is written once, exactly. - Disabling a header in its High Contrast hover/pressed state keeps the disabled foreground (state order fixed); the disabled sort glyph follows the text brush across theme changes. - A `CanSort` opt-out made inside `BeginningEdit` is replayed after the edit it let open closes, and rebuilds headers even when a `Sorted` handler throws. - Header rebuilds caused by `Content`/`Header` or `Columns` changes keep focus in the header band, respect an app's own focus move, and release `UIElement` headers back to the app. - A disabled group header does not dim its item count twice. The count opacity follows `ContentTemplate` changes while the header is disabled. <details><summary>Product changes by file</summary> | File | +/- | What changed | Items | |---|---|---|---| | `TableView/TableView.cpp` (+ .h) | +684 / −382 | `RebuildHeaders` creates `TableViewColumnHeader` controls; applies `ColumnHeaderStyle`/`HeaderStyle` with TargetType/BasedOn checks; density MinHeight/Padding only when the Style doesn't set them<br>`ReleaseHeaderCells` and header-focus handling for removed/replaced columns, re-entrant rebuilds and app focus moves<br>Header bottom rule thickness set on `PART_HeaderRow`; header lines no longer follow `GridLinesVisibility`<br>Row prepare/clear refresh chrome once; `TryResizeColumnFromAutomation` writes small widths exactly | 64475625, 64475623, 64143451, 64652961, 63996584, 63790855 | | `TableView/TableViewColumnHeader.cpp` (+ .h; replaces `TableViewHeaderCell.h`) | +435 / −210 | New sealed templated `ContentControl` with read-only `Column`<br>Resolves `PART_ContentPresenter`, `PART_GridLine`, `PART_SortIndicatorHost`, `PART_ResizeGripperHost`; hosts the sort glyph and gripper<br>Drives `CommonStates`/`HighContrastStates`/`SortStates`; disabled sort glyph bound to the text brush | 64475625, 64475623 | | `TableView/TableViewRow.cpp` (+ .h) | +191 / −35 | Recycled rows keep their cells unless cell automation peers exist; chrome refreshed once; tooltips re-applied on recycle<br>`BeginCellEdit` restores display element, edit state and focus on throw | 64652961, 63787021 | | `TableView/TableView.xaml` | +128 / −18 | Default `TableViewColumnHeader` Style/template: typography setters, visual states, parts<br>Outer border uses `TabularSurfaceOuterBorderBrush`; header lines use `TabularSurfaceHeaderSeparatorBrush`<br>Group header: `TableViewGroupExpanderSpacing` (8), padding 8,8,12,8, key/count text styles, count inherits `Foreground` | 64475625, 64475623, 64475611, 64475629 | | `TableView/TableViewColumnHeaderAutomationPeer.cpp` (+ .h) | +127 / −9 | `ITransformProvider`: `Resize` converts physical px through the visual scale; `CanResize` tracks gripper availability (pinned → false)<br>`IsEnabled` follows the header; name read from the content presenter | 63996584, 64475625 | | `TableView/TableView_Sort.cpp` | +113 / −39 | `SortByColumn(None)` bypasses the `CanSort` gate; other sorts refused<br>Sort through a source owned by another table returns false; implicit clears keep the app's sort and raise no `Sorted`; app-lit columns left alone on opt-out | 63790855 | | `TableView/TableView_Layout.cpp` | +74 / −14 | Realized-row walk only when a visible Auto column exists<br>Transform `CanResize` changes raised for existing header peers | 64652961, 63996584 | | `TableView/TableView_Editing.cpp` | +63 / −15 | `BeginEdit` unwinds on throw; `CanSort` opt-out during `BeginningEdit` queued until the edit closes | 63787021, 63790855 | | `TableView/TableViewCellAutomationPeer.cpp` (+ .h, `TableViewCell.h`) | +48 / −0 | Public constructor throws `E_INVALIDARG` for null args, negative/out-of-range index or a foreign column; internal path uses `TrustedOwnerTag` | 63787021 | | `TableView/TableViewGroupHeader.cpp` (+ .h) | +39 / −0 | Default count's opacity is not applied twice when disabled | 64475629 | | `TableView/TableView.idl` | +31 / −13 | `TableViewColumnHeader`, `TableView.ColumnHeaderStyle`, `TableViewColumn.HeaderStyle`<br>`CanSort`/`SortByColumn`/`ToggleSortDirection`/`GridLinesVisibility` docs; cell-peer `E_INVALIDARG`; group-header parts | 64475625, 63790855, 64143451, 63787021 | | `TableView/TableView_Columns.cpp` | +19 / −0 | Releases removed columns' header cells; old `UIElement` `Header` returned to the app on change | 64475625 | | `TableView/TableViewColumn.cpp` | +7 / −0 | `HeaderStyle` change queues a header rebuild | 64475625 | | `TableView/TableViewSource.h` | +6 / −0 | `IsOwnedByOther` helper for sort sync | 63790855 | | `CommonStyles/TabularSurfaces_themeresources.xaml` | +5 / −2 | New `TabularSurfaceOuterBorderBrush` (Dark/Light/HC); group count opacity documented as secondary-over-primary text (0.77 Dark) | 64475611, 64475629 | | `TableView/TableView.vcxitems` | +2 / −1 | Swap `TableViewHeaderCell.h` for `TableViewColumnHeader.*` | 64475625 | | Generated / metadata (`Generated/TableView.properties.*`, `Generated/TableViewColumn.properties.*`, `dll-tabular/XamlMetadataProviderGenerated.h`) | +123 / −0 | Codegen for the new properties and type | 64475625 | **Total: 17 rows, 29 product files, +2117 / −738.** Also: 19 test files (+3718 / −32), 3 docs (+161 / −35), 6 sample files (+2 / −9). </details> ### New preview API (needs API review) - **`TableViewColumnHeader`**: sealed, templated `ContentControl`, created only by `TableView`. - Read-only `Column` property. - Template parts (optional): `PART_ContentPresenter`, `PART_GridLine`, `PART_SortIndicatorHost`, `PART_ResizeGripperHost`. - Visual state groups: `CommonStates`, `HighContrastStates`, `SortStates`. - **`TableView.ColumnHeaderStyle`**: Style applied to every column header. - **`TableViewColumn.HeaderStyle`**: per-column Style; wins over `TableView.ColumnHeaderStyle`. There is no `HeaderGridLinesVisibility` property. `GridLinesVisibility` affects body lines only; header lines are always drawn. To hide them, override `TabularSurfaceHeaderSeparatorBrush` with `Transparent`. ### Current Behavior - Headers are built in C++ with no template, no visual states and forced local typography, so apps can't restyle them. Header separators don't show. - Header peers report `HeaderItem` without Transform, so UIA clients can't resize columns. - `SortByColumn` ignores `CanSort`; `CanSort=false` on a sorted column leaves the sort in place; sorting through a source another table owns reports success; some implicit clears raise `Sorted(None)` while the app's sort remains. - The cell peer constructor accepts invalid arguments; a throw in `BeginEdit` leaves edit state half-open. - Recycling a row rebuilt every cell, refreshed chrome up to twice and invalidated the table's measure. - The outer border has no dedicated brush; the default group header uses raw weights and off-grid spacing. ### New Behavior - Headers use the templated `TableViewColumnHeader` (see API above); typography, padding and brushes come from Style setters. - Header peers implement `ITransformProvider` through the existing resize path (Min/Max clamping, scale conversion; pinned columns report `CanResize=false`). - `CanSort=false` blocks header clicks, `SortByColumn` (except `None`) and `ToggleSortDirection`, and clears the column's own active sort; sorts through a source owned by another table return false; `Sorted` is raised only when the table's sort state changes. - Cell peer constructor throws `E_INVALIDARG` on bad input; `BeginEdit` rolls back on failure. - Recycled rows reuse their cells unless cell automation peers exist; chrome refreshes once. > **Visible changes** > - Outer border uses `CardStrokeColorDefault`. > - Header lines are always shown, even when `GridLinesVisibility` is `None` or `Horizontal`. > - Group header restyled: 8px spacing; the count follows the header `Foreground`. > - Disabled header text and sort glyph dim. > - TableView samples no longer opt out of High Contrast backplates app-wide. ## Customer Impact Restylable column headers, visible header separators, Fluent-aligned outer border and group headers, UIA column resize for assistive technology, smoother scrolling and predictable sort events. ## Regression Potential - [ ] Low risk — isolated change, limited scope - [x] Medium risk — touches shared components or public APIs - [ ] High risk — architectural or breaking API change Header construction (focus restore, frozen columns, UIA peer, tooltips, resize, sort glyph) moved to a templated control. Sort event semantics are tighter: `CanSort` is enforced on programmatic sorts and fewer `Sorted` events fire. Outer border color and always-on header lines change visibly. Covered by new and existing tests. ## How Has This Been Tested? - [x] I have performed a self-review of my own code - [x] I have added tests to cover my changes - [x] Existing tests pass locally Full TableView API and interaction suites on test VMs: 877/882 pass at 224d10d5c; 3 Localization tests are [Ignore] and 2 intermittent failures passed 3/3 on rerun. Product, test apps and TableView sample apps build (x64 chk). Also covered: `VerifyDisabledGroupHeaderCountFollowsContentTemplateChanges`. Key new tests: - `VerifyColumnHeaderTemplateParts`, `VerifyColumnHeaderSortStates`, `VerifyColumnHeaderTypographyHasNoLocalValues` - `VerifyColumnHeaderStyleApplies`, `VerifyBasedOnOnlyColumnHeaderStyleApplies`, `VerifyIncompatibleColumnHeaderStyleIsIgnored` - `VerifyHeaderGridLinesShowWhateverTheBodyGridLines`, `VerifyTransparentHeaderSeparatorBrushHidesHeaderLines`, `VerifyGridLineAndOuterBorderBrushesMatchFluentStrokeTokens` - `VerifyDisablingHighContrastHoveredHeaderKeepsDisabledForeground`, `VerifyDefaultGroupHeaderCountInheritsForegroundThatFollowsStates` - `VerifyTransformResizeAccountsForAncestorScale`, `VerifyTransformResizeSmallWidthChangeWritesOnce`, `VerifyHeaderDisabledByStyleIsNotEnabledOrInvokable` - `VerifySortByColumnRespectsCanSort`, `VerifyImplicitClearKeepsSourceSort`, `VerifyUngatingKeepsAppLitColumnWithoutEvents`, `VerifySortOptOutDuringBeginningEditKeepsTheEditOpen` - `VerifyRecycledRowWithoutCellPeersReusesItsCells`, `VerifyHeaderFocusStaysInHeaderBandWhenColumnsReplaced`, `VerifyBeginEditThatThrowsRollsBackAndAllowsALaterEdit`, `VerifyCellPeerConstructorValidatesArguments` --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 94961bb8-d187-483c-bece-19e203752c67
728 lines
28 KiB
C++
728 lines
28 KiB
C++
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License. See LICENSE in the project root for license information.
|
|
|
|
#include "pch.h"
|
|
#include "common.h"
|
|
#include "TableView.h"
|
|
#include "TableViewColumn.h"
|
|
#include "TableViewRow.h"
|
|
#include "TableViewGroupHeader.h"
|
|
#include "TableViewCell.h"
|
|
#include "TableViewColumnHeader.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <vector>
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Column-width layout engine (all of it lives here).
|
|
//
|
|
// TableView owns the column-width policy; rows and headers are thin plumbing. Each cell host panel
|
|
// measures its Auto-column children unconstrained and caches those measured widths by column;
|
|
// Pixel/Star cells are measured once at their resolved width (no unconstrained probe), mirroring
|
|
// CGrid. After the template subtree measures, TableView::MeasureOverride pulls those cached values
|
|
// from the header host and realized rows once, then resolves Pixel / Auto / Star widths into
|
|
// TableViewColumn.ActualWidth.
|
|
//
|
|
// Only realized rows contribute, so Auto sizes to the widest cell realized so far. Auto is grow-only
|
|
// within a data set, so a column never narrows as rows scroll out (ResetColumnDesiredWidths).
|
|
//
|
|
// Resolved widths (Pixel / Auto / Star) are snapped to device pixels when UseLayoutRounding is set,
|
|
// using the XamlRoot rasterization scale -- again mirroring CGrid.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
namespace
|
|
{
|
|
bool IsFixedWidthColumn(winrt::TableViewColumn const& column)
|
|
{
|
|
const double lo = NonNegativeFiniteOrZero(column.MinWidth());
|
|
return MaxWidthForColumn(column, lo) <= lo;
|
|
}
|
|
|
|
double MinWidthForStarFactor(winrt::TableViewColumn const& column, double factor)
|
|
{
|
|
if (factor > 0.0)
|
|
{
|
|
return NonNegativeFiniteOrZero(column.MinWidth());
|
|
}
|
|
|
|
// WPF gives 0* a zero share. Preserve that for the default MinWidth, but still honor an
|
|
// explicitly-set MinWidth on a 0* column.
|
|
const auto localMinWidth = column.ReadLocalValue(winrt::TableViewColumn::MinWidthProperty());
|
|
return localMinWidth == winrt::DependencyProperty::UnsetValue()
|
|
? 0.0
|
|
: NonNegativeFiniteOrZero(column.MinWidth());
|
|
}
|
|
|
|
// Divides `available` in proportion to each column's Star factor. A column that would clamp at
|
|
// its Min/MaxWidth is fixed there and dropped, then the rest re-divide what is left (the WPF
|
|
// ComputeStarColumnWidths shape).
|
|
template <typename TFactor, typename TRound, typename TResolve>
|
|
void DistributeStarWidths(
|
|
std::vector<winrt::TableViewColumn> pool,
|
|
double available,
|
|
TFactor&& factorOf,
|
|
TRound&& layoutRound,
|
|
TResolve&& resolve)
|
|
{
|
|
auto totalFactor = [&]()
|
|
{
|
|
double total = 0.0;
|
|
for (auto const& c : pool)
|
|
{
|
|
total += NonNegativeFiniteOrZero(factorOf(c));
|
|
}
|
|
return total;
|
|
};
|
|
|
|
for (bool adjusted = true; adjusted && !pool.empty(); )
|
|
{
|
|
adjusted = false;
|
|
|
|
const double factorSum = totalFactor();
|
|
const double unit = factorSum > 0.0 ? available / factorSum : 0.0;
|
|
|
|
for (size_t i = 0; i < pool.size(); ++i)
|
|
{
|
|
auto const& c = pool[i];
|
|
const double factor = NonNegativeFiniteOrZero(factorOf(c));
|
|
const double desired = unit * factor;
|
|
const double lo = MinWidthForStarFactor(c, factor);
|
|
const double hi = MaxWidthForColumn(c, lo);
|
|
const double clamped = std::clamp(desired, lo, hi);
|
|
// std::clamp returns desired exactly when it is already in [lo, hi], so any
|
|
// inequality is a real Min/MaxWidth clamp.
|
|
if (clamped != desired)
|
|
{
|
|
resolve(c, clamped);
|
|
available -= clamped;
|
|
pool.erase(pool.begin() + i);
|
|
adjusted = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const double factorSum = totalFactor();
|
|
const double unit = factorSum > 0.0 ? std::max(0.0, available) / factorSum : 0.0;
|
|
// Rounded on the running total rather than per column, so the widths still sum to
|
|
// `available` once snapped; rounding each independently can overshoot the viewport and
|
|
// leave a permanent one-pixel scrollbar.
|
|
double exactConsumed = 0.0;
|
|
double roundedConsumed = 0.0;
|
|
for (auto const& c : pool)
|
|
{
|
|
const double factor = NonNegativeFiniteOrZero(factorOf(c));
|
|
const double lo = MinWidthForStarFactor(c, factor);
|
|
const double hi = MaxWidthForColumn(c, lo);
|
|
exactConsumed += std::clamp(unit * factor, lo, hi);
|
|
const double edge = layoutRound(exactConsumed);
|
|
resolve(c, edge - roundedConsumed);
|
|
roundedConsumed = edge;
|
|
}
|
|
}
|
|
}
|
|
|
|
winrt::Size TableView::MeasureOverride(winrt::Size const& availableSize)
|
|
{
|
|
auto desired = __super::MeasureOverride(availableSize);
|
|
ResolveColumnWidths();
|
|
QueueTerminalGridLineRefresh();
|
|
return desired;
|
|
}
|
|
|
|
// Requested from a cell panel's MeasureOverride when a realized cell's own measured width changed
|
|
// (grow or shrink). The request comes from below the body ScrollViewer, which absorbs the child's
|
|
// measure invalidation, so TableView::MeasureOverride (and ResolveColumnWidths) would not otherwise
|
|
// re-run. Invalidate our measure SYNCHRONOUSLY (not via the DispatcherQueue): InvalidateMeasure only
|
|
// marks us dirty, so the layout manager re-measures the TableView within the SAME layout tick -- the
|
|
// column resizes in the same frame as the content change (no one-frame lag, so drags stay smooth).
|
|
// It converges without a debounce: the delta detector records each cell's new width in the pass that
|
|
// detects it, so the follow-up measure sees no delta and stops. Calling InvalidateMeasure during a
|
|
// descendant's measure is safe -- it schedules, it does not re-enter layout.
|
|
void TableView::RequestColumnWidthResolve()
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
bool TableView::HasVisibleAutoColumn()
|
|
{
|
|
if (auto const columns = Columns())
|
|
{
|
|
for (auto const& column : columns)
|
|
{
|
|
if (column &&
|
|
column.Visibility() == winrt::Visibility::Visible &&
|
|
column.Width().GridUnitType == winrt::GridUnitType::Auto &&
|
|
winrt::get_self<TableViewColumn>(column)->GetOwningTableView() == *this)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// A newly prepared row's first measure raises no width-change signal, so Auto columns need this
|
|
// table pass to pick its cells up.
|
|
void TableView::InvalidateMeasureForRowRealizationChange()
|
|
{
|
|
if (HasVisibleAutoColumn())
|
|
{
|
|
InvalidateMeasure();
|
|
}
|
|
}
|
|
|
|
// Reset the monotonic Auto desired widths. The next table measure pass remeasures the template
|
|
// subtree, pulls header + realized-row measured widths, and resolves once from those caches.
|
|
void TableView::ResetColumnDesiredWidths()
|
|
{
|
|
if (auto columns = Columns())
|
|
{
|
|
for (auto const& column : columns)
|
|
{
|
|
// Reset EVERY column's grow-only accumulator regardless of its current Width mode. A
|
|
// column that was Auto, temporarily switched to Pixel/Star, then switched back must not
|
|
// retain a stale max from a previous data set. This runs only on real data-set boundaries
|
|
// (ItemsSource / Columns replaced / CellTemplate / Header), so it correctly clears across
|
|
// them while Density / Min / Max / HeadersVisibility changes (which do not call this)
|
|
// preserve the max within a data set.
|
|
if (column && winrt::get_self<TableViewColumn>(column)->GetOwningTableView() == *this)
|
|
{
|
|
winrt::get_self<TableViewColumn>(column)->ResetDesiredWidthInternal();
|
|
}
|
|
}
|
|
}
|
|
|
|
InvalidateMeasure();
|
|
}
|
|
|
|
// Resolve every visible column into ActualWidth. Fixed columns (Pixel + measured Auto) are sized
|
|
// first; Star columns then split the viewport width left over between them.
|
|
void TableView::ResolveColumnWidths()
|
|
{
|
|
auto columns = Columns();
|
|
if (!columns)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Snap resolved column widths to device pixels when layout rounding is enabled, mirroring CGrid's
|
|
// use of the rasterization scale. A zero factor leaves values unrounded.
|
|
double layoutRoundFactor = 0.0;
|
|
if (UseLayoutRounding())
|
|
{
|
|
if (auto const xamlRoot = XamlRoot())
|
|
{
|
|
layoutRoundFactor = xamlRoot.RasterizationScale();
|
|
}
|
|
}
|
|
auto layoutRound = [layoutRoundFactor](double value)
|
|
{
|
|
return layoutRoundFactor > 0.0 ? std::round(value * layoutRoundFactor) / layoutRoundFactor : value;
|
|
};
|
|
|
|
auto setResolvedActualWidth = [&layoutRound](winrt::TableViewColumn const& target, double resolved)
|
|
{
|
|
resolved = layoutRound(resolved);
|
|
const bool changed = std::abs(resolved - target.ActualWidth()) > 0.0001;
|
|
winrt::get_self<TableViewColumn>(target)->SetResolvedActualWidthInternal(resolved);
|
|
return changed;
|
|
};
|
|
|
|
double fixedTotal = 0.0;
|
|
bool changed = false;
|
|
std::vector<winrt::TableViewColumn> starColumns;
|
|
|
|
// Collect the currently realized, BOUND rows ONCE (a single repeater walk) and reuse the list for
|
|
// every Auto column below. Lazy, so tables without an Auto column skip the walk entirely.
|
|
std::vector<winrt::TableViewRow> realizedRows;
|
|
bool realizedRowsCollected = false;
|
|
auto ensureRealizedRows = [this, &realizedRows, &realizedRowsCollected]()
|
|
{
|
|
if (realizedRowsCollected)
|
|
{
|
|
return;
|
|
}
|
|
realizedRowsCollected = true;
|
|
if (auto repeater = m_rowsRepeater.get())
|
|
{
|
|
ForEachRealizedRow([&realizedRows, &repeater](winrt::TableViewRow const& row)
|
|
{
|
|
// Skip pooled rows (GetElementIndex < 0): their stale widths would re-pin Auto
|
|
// columns after ItemsSource shrinks.
|
|
if (repeater.GetElementIndex(row) >= 0)
|
|
{
|
|
realizedRows.push_back(row);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
for (auto const& column : columns)
|
|
{
|
|
// Rejected columns still appear in the app-owned vector; do not resolve widths for another
|
|
// TableView's column.
|
|
if (!column ||
|
|
winrt::get_self<TableViewColumn>(column)->GetOwningTableView() != *this ||
|
|
column.Visibility() != winrt::Visibility::Visible)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto width = column.Width();
|
|
const double lo = NonNegativeFiniteOrZero(column.MinWidth());
|
|
const double hi = MaxWidthForColumn(column, lo);
|
|
|
|
switch (width.GridUnitType)
|
|
{
|
|
case winrt::GridUnitType::Star:
|
|
// Sized below once the fixed total is known.
|
|
starColumns.push_back(column);
|
|
break;
|
|
|
|
case winrt::GridUnitType::Auto:
|
|
{
|
|
// Pull measured widths from the header and currently realized row panels. Rows and header
|
|
// are tracked separately, so toggling the headers never drops the rows' width.
|
|
double headerMeasured = 0.0;
|
|
// Only consume the header host's cache when headers are actually shown: a hidden header is
|
|
// not measured this pass, so its per-pass cache is stale (and, with the raw-identity key,
|
|
// a freed column's address could be reused by a new column and false-match a stale entry).
|
|
// A hidden header must not drive column width regardless.
|
|
if (ShouldShowColumnHeaders())
|
|
{
|
|
headerMeasured = NonNegativeFiniteOrZero(GetHeaderMeasuredWidthForColumn(column));
|
|
}
|
|
|
|
double rowsMeasured = 0.0;
|
|
ensureRealizedRows();
|
|
for (auto const& row : realizedRows)
|
|
{
|
|
// Ask the row for its cell's measured width instead of reaching into its panel.
|
|
rowsMeasured = std::max(rowsMeasured, winrt::get_self<TableViewRow>(row)->MeasuredWidthForColumn(column));
|
|
}
|
|
|
|
auto columnImpl = winrt::get_self<TableViewColumn>(column);
|
|
// Rows are grow-only within a data set (WPF / WCT DataGrid parity), so scrolling never
|
|
// narrows the column. Headers are never virtualized, so the current measurement is used.
|
|
columnImpl->SetDesiredWidthInternal(
|
|
std::max(columnImpl->RowsDesiredWidthInternal(), NonNegativeFiniteOrZero(rowsMeasured)),
|
|
headerMeasured);
|
|
const double desired = columnImpl->DesiredWidthInternal();
|
|
|
|
const double resolved = layoutRound(
|
|
std::clamp(desired > 0.0 ? desired : c_widthDefault.Value, lo, hi));
|
|
changed |= setResolvedActualWidth(column, resolved);
|
|
fixedTotal += resolved;
|
|
break;
|
|
}
|
|
|
|
case winrt::GridUnitType::Pixel:
|
|
default:
|
|
{
|
|
// A locked column the authored pass below sizes from its Star share must not also be
|
|
// booked as fixed here, or it lands in fixedTotal twice and starves the real donors.
|
|
if (CanUserResizeColumns() && !column.CanResize() &&
|
|
winrt::get_self<TableViewColumn>(column)->AuthoredWidthInternal().GridUnitType == winrt::GridUnitType::Star)
|
|
{
|
|
starColumns.push_back(column);
|
|
break;
|
|
}
|
|
|
|
const double desired = NonNegativeFiniteOrDefault(width.Value);
|
|
const double resolved = layoutRound(
|
|
std::clamp(desired, lo, hi));
|
|
changed |= setResolvedActualWidth(column, resolved);
|
|
fixedTotal += resolved;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (starColumns.empty())
|
|
{
|
|
if (changed)
|
|
{
|
|
InvalidateCellPanels();
|
|
RefreshFrozenColumns();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Star needs a finite viewport to divide. The horizontally-scrolling body panel is measured at
|
|
// infinite width, so we pull the ScrollViewer viewport explicitly. Before it is known, leave the
|
|
// Star columns at their provisional width; the body-scroller SizeChanged re-resolves later.
|
|
auto bodyScroller = m_bodyScroller.get();
|
|
const double viewport = bodyScroller ? bodyScroller.ViewportWidth() : 0.0;
|
|
// A non-finite or not-yet-known viewport has no finite space to divide (e.g. the table hosted
|
|
// in a width-to-content parent); leave Star columns at their provisional width rather than
|
|
// arranging an infinite cell. The body scroller's SizeChanged re-resolves once a real width lands.
|
|
if (!(viewport > 0.0) || std::isinf(viewport))
|
|
{
|
|
if (changed)
|
|
{
|
|
InvalidateCellPanels();
|
|
RefreshFrozenColumns();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const bool resizeEnabled = CanUserResizeColumns();
|
|
auto authoredOf = [](winrt::TableViewColumn const& c)
|
|
{
|
|
return winrt::get_self<TableViewColumn>(c)->AuthoredWidthInternal();
|
|
};
|
|
// Only while a resize can actually happen; otherwise no column's width is at risk and the
|
|
// ordinary pool already clamps correctly.
|
|
auto isLocked = [resizeEnabled](winrt::TableViewColumn const& c)
|
|
{
|
|
return resizeEnabled && !c.CanResize();
|
|
};
|
|
|
|
if (std::any_of(starColumns.begin(), starColumns.end(), isLocked))
|
|
{
|
|
// A locked Star column takes its share of the authored layout rather than of whatever a
|
|
// neighbor's resize left behind, so a drag cannot change its width.
|
|
double authoredFixedTotal = 0.0;
|
|
std::vector<winrt::TableViewColumn> authoredPool;
|
|
for (auto const& c : columns)
|
|
{
|
|
if (!c ||
|
|
winrt::get_self<TableViewColumn>(c)->GetOwningTableView() != *this ||
|
|
c.Visibility() != winrt::Visibility::Visible)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto authored = authoredOf(c);
|
|
if (authored.GridUnitType == winrt::GridUnitType::Star)
|
|
{
|
|
authoredPool.push_back(c);
|
|
continue;
|
|
}
|
|
|
|
// Clamped and rounded exactly as the resolved pass above does, so the authored basis
|
|
// and the real layout agree on what the fixed columns take.
|
|
const double lo = NonNegativeFiniteOrZero(c.MinWidth());
|
|
const double hi = MaxWidthForColumn(c, lo);
|
|
if (authored.GridUnitType == winrt::GridUnitType::Auto)
|
|
{
|
|
// The content width, not the width a resize gave it: a dragged Auto column must not
|
|
// change what the authored layout leaves for the Star columns.
|
|
const double desired = winrt::get_self<TableViewColumn>(c)->DesiredWidthInternal();
|
|
authoredFixedTotal += layoutRound(std::clamp(desired > 0.0 ? desired : c.ActualWidth(), lo, hi));
|
|
}
|
|
else
|
|
{
|
|
const double desired = NonNegativeFiniteOrDefault(authored.Value);
|
|
authoredFixedTotal += layoutRound(
|
|
std::clamp(desired, lo, hi));
|
|
}
|
|
}
|
|
|
|
DistributeStarWidths(
|
|
authoredPool,
|
|
std::max(0.0, layoutRound(viewport) - authoredFixedTotal),
|
|
[&authoredOf](winrt::TableViewColumn const& c) { return std::max(0.0, authoredOf(c).Value); },
|
|
layoutRound,
|
|
[&](winrt::TableViewColumn const& c, double width)
|
|
{
|
|
if (isLocked(c))
|
|
{
|
|
changed |= setResolvedActualWidth(c, width);
|
|
fixedTotal += c.ActualWidth();
|
|
}
|
|
});
|
|
|
|
starColumns.erase(
|
|
std::remove_if(starColumns.begin(), starColumns.end(), isLocked), starColumns.end());
|
|
}
|
|
|
|
// The viewport basis is layout-rounded so the divided space is snapped consistently with the
|
|
// fixed columns (CGrid rounds availableSize before distribution); per-column Star widths are
|
|
// then snapped in setResolvedActualWidth.
|
|
DistributeStarWidths(
|
|
starColumns,
|
|
std::max(0.0, layoutRound(viewport) - fixedTotal),
|
|
[](winrt::TableViewColumn const& c) { return std::max(0.0, c.Width().Value); },
|
|
layoutRound,
|
|
[&](winrt::TableViewColumn const& c, double width)
|
|
{
|
|
changed |= setResolvedActualWidth(c, width);
|
|
});
|
|
|
|
if (changed)
|
|
{
|
|
InvalidateCellPanels();
|
|
RefreshFrozenColumns();
|
|
}
|
|
}
|
|
|
|
// Puts Width back the way the app left it. Restoring the effective value would convert a binding
|
|
// or an inherited default into a local value the app never set.
|
|
void TableView::RestoreColumnWidth(const winrt::TableViewColumn& column, const winrt::IInspectable& localWidth)
|
|
{
|
|
auto const columnImpl = winrt::get_self<TableViewColumn>(column);
|
|
auto const resizeScope = columnImpl->BeginUserResizeScope();
|
|
if (!localWidth || localWidth == winrt::DependencyProperty::UnsetValue())
|
|
{
|
|
column.ClearValue(winrt::TableViewColumn::WidthProperty());
|
|
}
|
|
else
|
|
{
|
|
column.SetValue(winrt::TableViewColumn::WidthProperty(), localWidth);
|
|
}
|
|
}
|
|
|
|
// A resize takes space only from the columns after the dragged one (the WPF DataGrid contract).
|
|
// Holding the earlier Star columns at the width they already render keeps them out of the
|
|
// redistribution pass without changing what the user sees. A locked column is skipped: the
|
|
// authored pass below already pins it, and freezing it would double-count it there.
|
|
void TableView::FreezeColumnsBeforeResize(const winrt::TableViewColumn& column, std::vector<ColumnResizeFrozenColumn>& frozen)
|
|
{
|
|
auto columns = Columns();
|
|
uint32_t index = 0;
|
|
if (!columns || !columns.IndexOf(column, index))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Collected before any write: writing Width runs app callbacks that may mutate Columns, and
|
|
// indexing a live vector across that would throw out of the manipulation.
|
|
std::vector<winrt::TableViewColumn> candidates;
|
|
for (uint32_t i = 0; i < index && i < columns.Size(); ++i)
|
|
{
|
|
auto const other = columns.GetAt(i);
|
|
if (!other ||
|
|
winrt::get_self<TableViewColumn>(other)->GetOwningTableView() != *this ||
|
|
other.Visibility() != winrt::Visibility::Visible ||
|
|
other.Width().GridUnitType != winrt::GridUnitType::Star ||
|
|
!other.CanResize())
|
|
{
|
|
continue;
|
|
}
|
|
candidates.push_back(other);
|
|
}
|
|
|
|
for (auto const& other : candidates)
|
|
{
|
|
frozen.push_back({ winrt::make_weak(other), other.ReadLocalValue(winrt::TableViewColumn::WidthProperty()) });
|
|
|
|
auto const columnImpl = winrt::get_self<TableViewColumn>(other);
|
|
auto const resizeScope = columnImpl->BeginUserResizeScope();
|
|
other.Width(winrt::GridLengthHelper::FromPixels(other.ActualWidth()));
|
|
}
|
|
}
|
|
|
|
// How far a drag may take this column. A table whose columns divide the viewport may not grow past
|
|
// it, and a column the user may not resize neither gives width away nor takes any.
|
|
ColumnResizeBounds TableView::ResizeBoundsForColumn(const winrt::TableViewColumn& column)
|
|
{
|
|
ColumnResizeBounds bounds;
|
|
|
|
auto columns = Columns();
|
|
auto bodyScroller = m_bodyScroller.get();
|
|
const double viewport = bodyScroller ? bodyScroller.ViewportWidth() : 0.0;
|
|
if (!columns || !(viewport > 0.0) || std::isinf(viewport))
|
|
{
|
|
return bounds;
|
|
}
|
|
|
|
auto authoredType = [](winrt::TableViewColumn const& c)
|
|
{
|
|
return winrt::get_self<TableViewColumn>(c)->AuthoredWidthInternal().GridUnitType;
|
|
};
|
|
|
|
const bool resizeEnabled = CanUserResizeColumns();
|
|
uint32_t draggedIndex = 0;
|
|
if (!columns.IndexOf(column, draggedIndex))
|
|
{
|
|
return bounds;
|
|
}
|
|
|
|
double reservedForOthers = 0.0;
|
|
bool dividesViewport = authoredType(column) == winrt::GridUnitType::Star;
|
|
|
|
for (uint32_t i = 0; i < columns.Size(); ++i)
|
|
{
|
|
auto const other = columns.GetAt(i);
|
|
if (!other ||
|
|
i == draggedIndex ||
|
|
winrt::get_self<TableViewColumn>(other)->GetOwningTableView() != *this ||
|
|
other.Visibility() != winrt::Visibility::Visible)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
dividesViewport |= authoredType(other) == winrt::GridUnitType::Star;
|
|
|
|
// Only a column that is Star *now* and sits after the dragged one can yield space: the
|
|
// layout pass re-divides by current Width, and a resize never takes from its left.
|
|
const bool participates =
|
|
i > draggedIndex &&
|
|
other.Width().GridUnitType == winrt::GridUnitType::Star &&
|
|
resizeEnabled && other.CanResize() &&
|
|
!IsFixedWidthColumn(other);
|
|
|
|
if (!participates)
|
|
{
|
|
reservedForOthers += other.ActualWidth();
|
|
}
|
|
else
|
|
{
|
|
reservedForOthers += MinWidthForStarFactor(other, std::max(0.0, other.Width().Value));
|
|
}
|
|
}
|
|
|
|
// Without a Star column anywhere the extent is meant to grow and scroll.
|
|
if (!dividesViewport)
|
|
{
|
|
return bounds;
|
|
}
|
|
|
|
// Pinned in both directions: bounding only growth would let the drag hand width to a column
|
|
// that is then not allowed to give it back. A Pixel or Auto column owns its width outright, so
|
|
// it may still shrink -- that only makes the table narrower and needs nothing from a neighbour.
|
|
if (IsColumnResizePinned(column))
|
|
{
|
|
bounds.Min = column.ActualWidth();
|
|
bounds.Max = bounds.Min;
|
|
return bounds;
|
|
}
|
|
|
|
bounds.Max = std::max(0.0, viewport - reservedForOthers);
|
|
return bounds;
|
|
}
|
|
|
|
// MinWidth meets MaxWidth, or a Star column with no later resizable Star column to trade with.
|
|
// Shared by the drag bounds and gripper visibility so the two cannot disagree.
|
|
bool TableView::IsColumnResizePinned(const winrt::TableViewColumn& column)
|
|
{
|
|
if (!column)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsFixedWidthColumn(column))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (column.Width().GridUnitType != winrt::GridUnitType::Star)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto columns = Columns();
|
|
uint32_t columnIndex = 0;
|
|
if (!columns || !columns.IndexOf(column, columnIndex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!CanUserResizeColumns())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for (uint32_t i = columnIndex + 1; i < columns.Size(); ++i)
|
|
{
|
|
auto const other = columns.GetAt(i);
|
|
if (other &&
|
|
winrt::get_self<TableViewColumn>(other)->GetOwningTableView() == *this &&
|
|
other.Visibility() == winrt::Visibility::Visible &&
|
|
other.Width().GridUnitType == winrt::GridUnitType::Star &&
|
|
other.CanResize() &&
|
|
!IsFixedWidthColumn(other))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void TableView::RefreshResizeGripperAvailability()
|
|
{
|
|
auto const host = m_headerHost.get();
|
|
if (!host)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const children = host.Children();
|
|
const uint32_t count = children.Size();
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
auto const headerCell = children.GetAt(i).try_as<winrt::FrameworkElement>();
|
|
if (!headerCell)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const gripper = FindResizeGripperInCell(headerCell);
|
|
auto const column = headerCell.Tag().try_as<winrt::TableViewColumn>();
|
|
if (!gripper || !column)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const visibility = IsColumnResizePinned(column) ? winrt::Visibility::Collapsed : winrt::Visibility::Visible;
|
|
if (gripper.Visibility() == visibility)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only a peer a client already holds is notified; FromElement would create one.
|
|
auto const header = headerCell.try_as<winrt::TableViewColumnHeader>();
|
|
auto const headerPeer = header && winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::PropertyChanged)
|
|
? winrt::get_self<TableViewColumnHeader>(header)->GetExistingAutomationPeerInternal() : nullptr;
|
|
// The pin state is already updated, so the old value comes from the visibility being replaced.
|
|
const bool couldResize = gripper.Visibility() == winrt::Visibility::Visible && IsColumnResizeAllowed(column);
|
|
gripper.Visibility(visibility);
|
|
|
|
if (headerPeer)
|
|
{
|
|
if (const bool canResize = CanResizeColumnFromAutomation(column); canResize != couldResize)
|
|
{
|
|
headerPeer.RaisePropertyChangedEvent(
|
|
winrt::TransformPatternIdentifiers::CanResizeProperty(),
|
|
winrt::box_value(couldResize),
|
|
winrt::box_value(canResize));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-run the cell panels' measure/arrange so the header band and all rows reflect the newly resolved
|
|
// column widths (Auto growth in one row must widen the header and every other row).
|
|
void TableView::InvalidateCellPanels()
|
|
{
|
|
if (auto headerHost = m_headerHost.get())
|
|
{
|
|
headerHost.InvalidateMeasure();
|
|
}
|
|
|
|
if (auto repeater = m_rowsRepeater.get())
|
|
{
|
|
const auto childCount = winrt::VisualTreeHelper::GetChildrenCount(repeater);
|
|
for (int32_t i = 0; i < childCount; ++i)
|
|
{
|
|
if (auto row = winrt::VisualTreeHelper::GetChild(repeater, i).try_as<winrt::TableViewRow>())
|
|
{
|
|
// Ask the row to invalidate its own cell panel instead of reaching into it.
|
|
winrt::get_self<TableViewRow>(row)->InvalidateCells();
|
|
}
|
|
else if (auto header = winrt::VisualTreeHelper::GetChild(repeater, i).try_as<winrt::TableViewGroupHeader>())
|
|
{
|
|
// The group-header band spans the same columns and sizes itself during measure, but
|
|
// it is a direct repeater child rather than a cells panel, so the row walk above
|
|
// never reaches it. Handled in this one walk so the two cannot drift apart.
|
|
UpdateGroupHeaderWidth(header);
|
|
}
|
|
}
|
|
}
|
|
}
|