Skip to content

Tizen alerts, modal navigation and gesture infrastructure - #9

Open
Redth wants to merge 11 commits into
redth-tizen-core-vertical-slicefrom
redth-tizen-alerts-gestures
Open

Tizen alerts, modal navigation and gesture infrastructure#9
Redth wants to merge 11 commits into
redth-tizen-core-vertical-slicefrom
redth-tizen-alerts-gestures

Conversation

@Redth

@Redth Redth commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Stacked on redth-tizen-core-vertical-slice. Implements the Controls platform layer for alerts, modal coordination, modal page navigation and gestures.

Built on the public extensibility contracts that shipped in .NET MAUI 11 — IAlertManager / IAlertManagerSubscription (#36633) and IGesturePlatformManager / IGesturePlatformManagerFactory (#36655) — plus a provisional alignment with the still-open modal seam (#37853). No DispatchProxy, no private reflection, no InternalsVisibleTo.

150 host-side tests, all green. Diff is 40 files, all within this slice.

Alerts and dialog coordination

TizenAlertManager implements IAlertManager; TizenAlertManagerSubscription implements IAlertManagerSubscription. Both port the NUI AlertRequestHelper. Registered scoped, because MAUI resolves IAlertManager from the per-window scope — that is what gives each window its own window-affine manager.

Why a full manager and not just a subscription. Native NUI popups must be dismissed explicitly. MAUI's built-in manager treats Unsubscribe as "drop the reference", which on Tizen leaves an orphaned modal popup on screen and the awaiting DisplayAlertAsync caller pending forever. TizenAlertRegistrationMode.SubscriptionOnly gives the built-in-manager path for hosts that prefer it. Both modes are tested.

Two deliberate deviations from the original NUI code, documented in place and pinned by tests:

  • Nested Page.IsBusy scopes keep the busy indicator open until the reference count reaches zero. The original closed it on the second "busy" notification, so overlapping scopes dismissed it early.
  • The dialog result is published after the modal placeholder is popped and the popup disposed, and unexpected failures fault the caller instead of being swallowed. Result and cancellation values are unchanged; this removes a re-entrancy window and a hang.

Modal page navigation — ⚠️ provisional

This was the one area that could not be built on the shipped .NET MAUI 11 surface. ModalNavigationManager.Tizen.cs upstream is an internal partial-class completion compiled into Microsoft.Maui.Controls, and the neutral Standard partial only updates logical state, so an out-of-tree backend gets no rendering and has no DI/factory/provider hook at all.

dotnet/maui#37853 adds that seam, following the shape already used by the alert and gesture seams. It is still open, so its interfaces are not in the 11.0.0-preview.7 package this repository builds against.

Core/Platform/Modal/ProvisionalModalNavigationContracts.cs carries copies with member shapes taken verbatim from the PR, so the implementation is written against the final contract today. Adopting the real interfaces is a namespace change on two types plus deleting that file — no logic moves.

The copies live in Microsoft.Maui.Platforms.Tizen, not Microsoft.Maui.Controls.Platform: re-declaring a MAUI type name in a MAUI namespace would collide (CS0433) for consumers that also reference MAUI's own build once the PR lands.

ProvisionalModalNavigationContractTests keeps the copies honest — it asserts each interface's member shape against the PR, asserts the namespace rule, and fails outright once Microsoft.Maui.Controls.Platform.IModalNavigationPlatform appears in the referenced assembly, with instructions to delete the provisional file. The copies cannot drift and cannot outlive their purpose.

TizenModalNavigationPlatform ports ModalNavigationManager.Tizen.cs onto the seam. SendDisappearing/SendAppearing and the manual _platformModalPages bookkeeping are deliberately absent: under the seam the framework owns the platform stack and raises the page lifecycle events, so keeping them would fire those events twice.

TizenModalPageRealizer replaces modal.ToPlatform(context), which is compiled per platform and has no Tizen build now that Tizen left the MAUI repo. It does the same work through public, platform-neutral handler APIs — which also makes page realization testable on the host.

Dialog coordination went neutral too: NuiModalHost is replaced by TizenModalHost driving the new ITizenNavigationStack. Placeholder balance — the failure mode that wedges every subsequent modal in the app — is now verified by host-side tests rather than only on device.

Details in docs/tizen-modal-navigation.md.

Gestures

TizenGesturePlatformManagerFactory implements IGesturePlatformManagerFactory and works with any IViewHandler via the standard PlatformView / ContainerView pair. The dependency on MAUI's Tizen-only IPlatformViewHandler shape is gone, and a test asserts the handler under test does not implement it.

Ported: GestureDetector, tap, pan, swipe, pinch, long press. Pointer is new — the original backend had no PointerGestureRecognizer support — derived from NUI touch and hover events. The Registrar assembly-scanning fallback is replaced by explicit DI seams.

⚠️ Public MAUI API gap

MAUI 11 exposes public gesture controller interfaces for pan, pinch and swipe only. TapGestureRecognizer.SendTapped, the LongPressGestureRecognizer send members and the entire PointerGestureRecognizer send surface are internal — verified by reflecting over the shipped assembly.

Detection is implemented in full; dispatch goes through one seam, ITizenGestureDispatcher, which raises what it can and reports the rest as unsupported without throwing — a view with a TapGestureRecognizer behaves as if it had no gesture rather than crashing. Tests assert those events do not fire, so they fail loudly when upstream opens the API.

Reported rather than worked around. Full detail and the suggested upstream shape in docs/tizen-gesture-support-matrix.md.

Second finding: dotnet/maui's LongPressGestureHandler.cs calls Tizen.NUI.LongPressGestureDetector.SetMinimumHoldingTime, which does not exist in TizenFX (checked against Samsung.Tizen.Ref API13 and API15). That source has not compiled since Tizen left the MAUI build. MinimumPressDuration is not honourable on Tizen and the call is not carried forward — do not restore it during a future upstream sync.

Testing

150 host-side tests covering DI registration and lifetimes, alert manager/subscription lifecycle including repeated subscribe-unsubscribe, window affinity, result and cancellation mapping for all three dialog types, modal push/pop ordering and animation flags, batch-pop suppression, back-button routing, dialog placeholder balance including fault and buried-placeholder paths, detector attach/detach and enable/transparent transitions, and gesture translation. Dispatch tests use real MAUI recognizers, not doubles.

The logic sits behind Tizen-owned contracts with NUI isolated under Core/Platform/Nui, so the test project source-includes the neutral half rather than referencing a neutral build of the product. No neutral product assembly is produced, so the single-TFM rule in Directory.Build.props is preserved and NoProjectFallsBackToANeutralTargetFramework still passes.

eng/verify-nui-sources.sh type-checks the NUI half against TizenFX reference assemblies without the Samsung workload. It has caught two real problems: the SetMinimumHoldingTime call above, and a back-button implementation that reached for SetBackButtonPressedHandler — a MAUI Core Tizen extension, not a NUI or Tizen.UIExtensions API.

Boundaries and dependencies on core types

None blocking; all degrade gracefully:

  • TizenNuiHostingExtensions.AttachTizenWindow(mauiContext, window, navigationStack, backButton?) — one call from the Tizen window handler wires window-affine alert routing, dialog coordination and modal navigation.
  • Back button is deliberately not implemented here. Upstream the registry lives in Microsoft.Maui.Platform.WindowExtensions and is consumed by MauiApplication, both Core-layer concerns. Duplicating it would create a second, competing source of truth for back-button routing, so it is an optional argument instead. Without it, back presses fall through to the platform default.
  • NuiAlertDialogFactory carries a private keyboard mapping duplicating Microsoft.Maui.Platform.KeyboardExtensions.ToPlatform. Swap to the Core one once Maui.Tizen.Core exposes it.

Device tests for the NUI adapters remain the one gap, blocked on the Samsung workload.

Drive-by fix

The workload gate in eng/build-workload-free.sh matched any workload id containing "tizen", so a machine with MAUI's own maui-tizen workload was reported as having the Samsung platform SDK. It does not — such a machine still fails NETSDK1139. Left as-is this would have promoted the Tizen lane to required and broken CI. Detection now mirrors the manifest probe in Directory.Build.props.

Not included

BlazorWebView — owned by another session.

Implements the Controls platform layer for alerts, modal coordination and
gestures on top of the extensibility contracts that shipped in .NET MAUI 11:
IAlertManager / IAlertManagerSubscription (dotnet/maui#36633) and
IGesturePlatformManager / IGesturePlatformManagerFactory (dotnet/maui#36655).
No DispatchProxy, no private reflection, no InternalsVisibleTo.

Alerts
------
TizenAlertManager implements IAlertManager and TizenAlertManagerSubscription
implements IAlertManagerSubscription, porting the NUI AlertRequestHelper from
dotnet/maui. Registered scoped, because MAUI resolves IAlertManager from the
per-window scope - that is what gives each window its own window-affine manager.

A full manager is supplied rather than only a subscription because native NUI
popups must be dismissed explicitly. MAUI's built-in manager treats Unsubscribe
as "drop the reference", which on Tizen would leave an orphaned modal popup on
screen and leave the awaiting DisplayAlertAsync caller pending forever.
TizenAlertRegistrationMode.SubscriptionOnly is available for hosts that prefer
the built-in manager's semantics.

Two deliberate deviations from the original NUI code, both documented in place:

  * Nested Page.IsBusy scopes now keep the busy indicator open until the
    reference count reaches zero. The original closed it on the second "busy"
    notification, so overlapping scopes dismissed it early.
  * The dialog result is published after the modal placeholder is popped and the
    popup disposed, and unexpected failures fault the caller instead of being
    swallowed. Result and cancellation values are unchanged.

Gestures
--------
TizenGesturePlatformManagerFactory implements IGesturePlatformManagerFactory and
works with any IViewHandler through the standard PlatformView/ContainerView pair.
The dependency on MAUI's Tizen-only IPlatformViewHandler shape is gone.

Ported: GestureDetector, tap, pan, swipe, pinch and long press. Pointer is new -
the original backend had no PointerGestureRecognizer support - and is derived
from NUI touch and hover events.

Public MAUI API gap
-------------------
MAUI 11 exposes public controller interfaces for pan, pinch and swipe only.
TapGestureRecognizer.SendTapped, the LongPressGestureRecognizer send members and
the whole PointerGestureRecognizer send surface remain internal, so an
out-of-tree backend cannot raise them without private reflection.

Detection is implemented in full and dispatch goes through one seam,
ITizenGestureDispatcher, which raises what it can and reports the rest as
unsupported without throwing. Tests assert that those events do not fire, so
they fail loudly once upstream opens the API. Reported rather than worked
around, per docs/tizen-gesture-support-matrix.md.

Also found: dotnet/maui's LongPressGestureHandler.cs calls
Tizen.NUI.LongPressGestureDetector.SetMinimumHoldingTime, which does not exist
in TizenFX (checked against Samsung.Tizen.Ref API13 and API15). That source has
not compiled since Tizen left the MAUI build. MinimumPressDuration is therefore
not honourable on Tizen; the call is not carried forward.

Testing
-------
101 host-side tests in tests/Controls.UnitTests covering DI registration and
lifetimes, manager and subscription lifecycle, window affinity, result and
cancellation mapping, modal-stack balance, detector attach/detach, and gesture
translation. Dispatch tests use real MAUI recognizers rather than doubles.

The alert and gesture logic sits behind Tizen-owned contracts with the NUI
implementations isolated under Core/Platform/Nui, so the test project
source-includes the neutral half rather than referencing a neutral build of the
product. No neutral product assembly is produced, so the single-TFM rule in
Directory.Build.props is preserved.

eng/verify-nui-sources.sh type-checks the NUI half against TizenFX reference
assemblies without the Samsung workload. That is how the SetMinimumHoldingTime
problem above was found.

Also fixes the workload gate in eng/build-workload-free.sh, which matched any
workload id containing "tizen" and so reported MAUI's own maui-tizen workload as
the Samsung platform SDK. A machine with only maui-tizen still cannot build
net11.0-tizen11.0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

CI is green on this branch.

Repository CI only triggers on PRs targeting main, so a stacked PR gets no automatic checks. Dispatched manually instead: run 33005740158 — all three jobs pass.

✓ Build and test (no Tizen workload)      31s
✓ Verify imported history                  7s
✓ Tizen workload availability (gate)      12s

  PASS repository invariant tests
  PASS controls platform tests
  PASS All workload-free checks passed

The controls platform tests line is the 101 new tests added here. Worth noting that the workload gate job correctly reports the Samsung SDK as unavailable now — before the fix in this PR it would have matched MAUI's own maui-tizen workload and claimed the Tizen lane was ready.

Something for the foundation to consider separately: extending the pull_request trigger beyond main would give stacked PRs automatic checks.

@Redth Redth closed this Aug 26, 2026
@Redth Redth reopened this Aug 26, 2026
Redth and others added 2 commits August 26, 2026 15:38
Both conflicts were purely additive and resolved as unions: the solution's
test folder and the workload-free lane's project list now carry the core
slice's Maui.Tizen.Core.UnitTests / Maui.Tizen.Core.RefPackCompile alongside
this branch's Controls.UnitTests.

This branch keeps the newer foundation commits (d22811b, 6ace22b, f8316cb)
that the core slice does not have yet, including the global.json pin to a
concrete SDK version. Dropping them by rebasing instead of merging would have
reintroduced the setup-dotnet failure that 6ace22b fixed.

All three test lanes pass on the merged tree.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Modal page navigation was the one area of this slice that could not be built on
the shipped .NET MAUI 11 surface. ModalNavigationManager.Tizen.cs upstream is an
internal partial-class completion compiled into Microsoft.Maui.Controls, and the
neutral Standard partial only updates logical state, so an out-of-tree backend
gets no rendering and has no DI, factory or provider hook.

dotnet/maui#37853 adds that seam, following the shape already used by
IAlertManager (#36633) and IGesturePlatformManagerFactory (#36655). It is still
OPEN, so its interfaces are not in the 11.0.0-preview.7 package this repository
builds against.

Provisional alignment
---------------------
Core/Platform/Modal/ProvisionalModalNavigationContracts.cs carries copies of
IModalNavigationPlatform, IModalNavigationPlatformFactory and
IModalNavigationHost with member shapes taken verbatim from the PR. Adopting the
real interfaces is then a namespace change on two types plus deleting that file.

The copies live in Microsoft.Maui.Platforms.Tizen, NOT
Microsoft.Maui.Controls.Platform: re-declaring a MAUI type name in a MAUI
namespace would collide (CS0433) for consumers that also reference MAUI's own
build once the PR lands.

ProvisionalModalNavigationContractTests keeps the copies honest. It asserts the
member shape of each interface, asserts the namespace rule, and fails outright
once Microsoft.Maui.Controls.Platform.IModalNavigationPlatform appears in the
referenced assembly, with instructions to delete the provisional file.

Implementation
--------------
TizenModalNavigationPlatform ports ModalNavigationManager.Tizen.cs onto the seam.
SendDisappearing/SendAppearing and the manual _platformModalPages bookkeeping are
deliberately absent: under the seam the framework owns the platform stack and
raises the page lifecycle events, so keeping them would fire those events twice.

Batch pops suppress animation so the intermediate modals of a Shell pop-to-root
do not flash, and the back-button handler resolves the current page on every
press rather than capturing it.

TizenModalPageRealizer replaces modal.ToPlatform(context), which is compiled per
platform and has no Tizen build now that Tizen left the MAUI repository. It does
the same work through public, platform-neutral handler APIs, which also makes
page realization testable on the host.

Dialog coordination is now neutral too
--------------------------------------
NuiModalHost is replaced by TizenModalHost, which drives the new Tizen-owned
ITizenNavigationStack instead of NUI directly. Placeholder balance - the failure
mode that wedges every subsequent modal in the app - is therefore verified by
host-side tests rather than only on device. NuiNavigationStack is now the single
NUI-aware piece of modal coordination.

Window-scoped services
----------------------
ITizenNavigationStack and ITizenWindowBackButton wrap objects the window owns,
but registration happens before any window exists, so both are registered scoped
as holders that the window handler fills in via AttachTizenWindow.

They fail differently on purpose: an unattached navigation stack throws, because
a modal that reports success without appearing is worse than a clear failure,
while an unattached back button records and replays the handler, because
PageAttached can run before the window handler does and a missing back button is
not fatal.

No back-button implementation is supplied here. Upstream that registry lives in
Microsoft.Maui.Platform.WindowExtensions and is consumed by MauiApplication, both
of which belong to the Tizen Core layer; duplicating it would create a second,
competing source of truth for back-button routing. AttachTizenWindow takes the
Core layer's implementation as an optional argument instead.

Testing
-------
150 host-side tests, up from 101. New coverage: modal push/pop ordering,
animation-flag propagation, batch-pop suppression, back-button routing and
disposal, factory per-window isolation and null-return fallback, dialog
placeholder balance including the fault and buried-placeholder paths,
window-scoped holder semantics, and the provisional contract guards.

eng/verify-nui-sources.sh now also type-checks the modal sources. It caught the
back-button boundary problem: NuiWindowBackButton was calling
SetBackButtonPressedHandler, which is a MAUI Core Tizen extension rather than a
NUI or Tizen.UIExtensions API and does not exist here.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth
Redth changed the base branch from redth-tizen-foundation-import to redth-tizen-core-vertical-slice August 26, 2026 19:49
@Redth Redth changed the title Tizen alerts, modal coordination and gesture infrastructure Tizen alerts, modal navigation and gesture infrastructure Aug 26, 2026
@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto the core vertical slice and retargeted; CI green.

Base: now redth-tizen-core-vertical-slice · Head: 0f8671c · CI: run 33007265034

✓ Build and test (no Tizen workload)      38s
✓ Verify imported history                  6s
✓ Tizen workload availability (gate)      14s

  PASS repository invariant tests
  PASS backend slice tests
  PASS controls platform tests
  PASS All workload-free checks passed

Merged rather than rebased, deliberately. fcf3cca branches from foundation 79aa0f3, but this branch was on f8316cb. Rebasing would have dropped three foundation commits the core slice does not have yet — including 6ace22b, which pinned global.json to a concrete SDK version to fix a setup-dotnet failure. Merging keeps both. Both conflicts (Maui.Tizen.slnx, eng/build-workload-free.sh) were purely additive and resolved as unions, so the workload-free lane now runs all three test projects.

The PR diff against the new base is exactly 40 files, all within this slice — no foundation or core-slice content leaks in.

Test count is 150, up from 101, with modal navigation added. Worth flagging one thing the CI-semantics lane caught locally before it reached CI: a method-group-to-object conversion (CS8974) that only fails under TreatWarningsAsErrors. eng/build-workload-free.sh setting ContinuousIntegrationBuild=true by default is doing real work.

Redth added a commit that referenced this pull request Aug 26, 2026
…policy

Ten further review findings. All were real; several were things that looked
correct while doing nothing.

Blazor package graph (verified against the feed, not assumed)

Microsoft.AspNetCore.Components.WebView was pinned to MAUI's version stamp
11.0.0-preview.7.26418.3. That version does not exist for that package - it is an
ASP.NET Core package on its own version line - so NuGet silently substituted
11.0.0-rc.1.26378.118 (NU1603) and then failed with NU1109 downgrades across the
graph. That substitution is where the "actual dependency is rc.1" report came
from; it was NuGet's fallback, not a declared dependency.

Microsoft.AspNetCore.Components.WebView.Maui 11.0.0-preview.7.26418.3 declares
Microsoft.AspNetCore.Authorization, Microsoft.AspNetCore.Components.WebView and
Microsoft.JSInterop at exactly 11.0.0-preview.7.26381.103 - which also matches the
SDK build pinned in global.json, so the whole graph stays on one ASP.NET Core
build. Pinned there. RC1 would have silently upgraded past what MAUI asked for.

Package source mapping now lists the prerelease patterns under BOTH sources.
NuGet restores only from sources declaring the longest matching pattern, so
Microsoft.AspNetCore.* under dotnet11 alone would have made every ASP.NET Core
package resolvable only from that feed (NU1100 for anything it lacks).

Added eng/tests/PackageGraphProbe: a net11.0 project whose only purpose is to make
restore prove these pins resolve. Necessary because the real consumers are Tizen
projects, and they cannot restore at all - the workload gate fires before Restore
- so a broken pin would otherwise stay invisible until Samsung ships. Verified it
reproduces the original NU1603/NU1109 failure.

CI workload probe could never succeed (#5)

`dotnet workload install tizen --skip-manifest-update` cannot work on a clean
runner: the Samsung manifest is third-party and side-loaded, so the workload ID is
not discoverable through public workload search, and --skip-manifest-update
guarantees failure because the manifest is exactly what is missing. A probe that
can never succeed would keep reporting "blocked" for weeks after the gate lifts.
Replaced with a feed probe for an 11.0.100-band manifest package, checking both ID
spellings. Installation, when it happens, goes through Samsung's supported path.

Dead configuration removed (#6, #10)

  - <Compile Update="**/*.Tizen.cs" /> had no metadata, so it changed nothing, and
    EnableDefaultCompileItems=false meant there were no items to update anyway. It
    read as meaningful configuration while being unreachable.
  - ValidateBaselineConsistency did substring matching on JSON inside MSBuild.
    Parsed validation already exists in the workload-free script and the invariant
    tests, which is where it belongs.

Orphan imported projects (#7)

GraphicsTester.Skia.Tizen.csproj was imported because its filename contains
"Tizen", but it cannot load here: its TFM is $(_MauiDotNetTfm)-tizen (a
dotnet/maui property that does not exist here, so the TFM evaluates to the
malformed "-tizen") and both ProjectReferences point at projects never imported.
Parked as .csproj.orphan - file and history intact, invisible to project
discovery - with samples/README.md documenting every orphan asset directory and an
invariant test asserting every .csproj is in the solution.

Analyzers and warnings (#8, #14)

  - Added Microsoft.CodeAnalysis.PublicApiAnalyzers (PrivateAssets=all) so the
    PublicAPI baselines are enforced rather than inert text.
  - Removed the Microsoft.SourceLink.GitHub pin; the SDK has bundled it since .NET 8.
  - Suppressed CS1591 in TizenPackage.props: the inherited sources are not
    uniformly documented, so the first project to start compiling would otherwise
    fail on hundreds of missing-comment errors under warnings-as-errors.
    Documentation is its own workstream, not a blocker for the first handler.

Licensing (#9)

git-filter-repo is vendored and load-bearing, so unlike the referenced-only
Samsung packages it IS redistributed. Added its upstream, version, MIT licence
text and a verification command to THIRD-PARTY-NOTICES.md.

CI hygiene (#11, #12)

  - `dotnet test --no-build` is skipped when a preceding build failed; running it
    against missing output buries the actual first failure in cascading noise.
  - Added a scheduled dependency-audit workflow. The strict policy stays (audit at
    `low`, warnings as errors - this ships to devices where patching is slow), but
    a newly published advisory now files an issue weekly instead of ambushing
    whoever opens the next unrelated PR. Suppression process documented, with a
    required justification and re-review date.

Naming and provenance (#13)

Replaced 'samsung.net.sdk.tizen.manifest-11.0.100' with the actual band contract
Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7, recording both ID spellings
(NuGet IDs are case-insensitive) and the probe results for each. Documented that
tizen11.0/API15 comes from Samsung workload PR #310 - verified, not assumed.

Also recorded that enabling Build.Tasks raises real SkiaSharp native-asset
packaging questions, flagged as an open decision rather than papered over.

Tests: 28 -> 33 invariants, plus the package graph probe wired into the required lane.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Both conflicts were in files core had reworked substantially, so core's version
was taken wholesale and only this branch's two additions were re-applied:
Maui.Tizen.Controls.UnitTests in the solution's test folder and in the
workload-free lane's project list and test block.

Nothing from this branch was preserved by preference in the process. In
particular the grep-based workload probe added here in 885dd6a is dropped in
favour of core's ReportTizenWorkload MSBuild target, which is a better fix: it
asks the same code path Directory.Build.props uses rather than reimplementing
the check in shell. The finding behind it survives as a regression fixture -
"maui-tizen alone is NOT Samsung's workload" in eng/tests/test-workload-detection.sh.

The controls platform tests now sit inside core's BUILD_OK guard, so a build
failure reports one clear error instead of cascading --no-build noise.

Verified on the merged tree: the full workload-free lane passes, including the
package graph probe and all eight workload detection regressions, and
eng/verify-nui-sources.sh still type-checks the NUI sources.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

Merged core 4eb9163; PR is MERGEABLE / CLEAN with fresh checks.

Head: 0b4d186 · CI: run 33012615133 — all 3 jobs pass, and this one was triggered by pull_request rather than manual dispatch, so stacked PRs get real checks now.

✓ Build and test (no Tizen workload)      39s
✓ Tizen workload availability (gate)      13s
✓ Verify imported history                  4s

  PASS unit tests
  PASS backend slice tests
  PASS controls platform tests
  PASS All workload-free checks passed

Conflict resolution: both conflicts were in files core had reworked substantially, so I took core's version wholesale and re-applied only this branch's two additions — Maui.Tizen.Controls.UnitTests in the solution test folder and in the lane's project list and test block. The diff on eng/build-workload-free.sh against core is now purely additive (7 added lines, nothing removed).

Nothing from this branch was preserved by preference. In particular my grep-based workload probe from 885dd6a is gone, replaced by core's ReportTizenWorkload MSBuild target — which is the better fix, since it asks the same code path Directory.Build.props uses instead of reimplementing the check in shell. Good to see the finding survive as a regression fixture (maui-tizen alone is NOT Samsung's workload) rather than as my patch.

The controls platform tests now also sit inside core's BUILD_OK guard, so a build failure reports one clear error instead of cascading --no-build noise.

Diff against the new base is still exactly 40 files, all within this slice. Test count unchanged at 150. eng/verify-nui-sources.sh still passes on the merged tree.

Redth and others added 3 commits August 26, 2026 16:57
eng/verify-nui-sources.sh did the right job by the wrong means: it curl'd
Samsung.Tizen.Ref.API13 into a temp directory and generated a throwaway project,
which meant a network dependency in the lane, a second ref-pack version to keep
straight, and a mechanism that existed nowhere else in the repository.

tests/Maui.Tizen.Core.RefPackCompile already solves this properly - PackageDownload
of the pinned Samsung.Tizen.Ref.API15, restored and cached through NuGet like any
other dependency. tests/Maui.Tizen.Controls.RefPackCompile is its sibling for the
Controls platform layer.

Separate project rather than more Compile items in the Core lane: these sources need
Microsoft.Maui.Controls, which that lane deliberately does not reference, and keeping
them apart mirrors the product's own Core/Controls split so a break points at the
right assembly.

Sources are listed folder by folder rather than globbed. Core/Platform still holds
raw dotnet/maui imports whose partial halves live in Microsoft.Maui.Controls and
cannot compile from here; a **/*.cs glob pulls those in and buries the lane in
errors that say nothing about the code it is meant to check.

Verified the lane still catches what the script caught: reintroducing the
Tizen.NUI.LongPressGestureDetector.SetMinimumHoldingTime call fails the build
against API15 with CS1061, and removing it passes again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
dotnet/maui#37420 and #37671 made most of the gesture dispatch surface public.
Measured by reflecting over the shipped assemblies rather than reading source:

  11.0.0-preview.7.26418.3   Tap (none)  Pointer (none)  LongPress (none)
  11.0.0-preview.7.26426.4   Tap SendTapped
                             Pointer SendPointerEntered/Exited/Moved/Pressed/Released
                             LongPress (none)

So the package pin moves from 26418.3 to 26426.4 (nuspec commit bedd1b18b7). All seven
MAUI packages were confirmed present at that stamp before bumping, and the pin is kept
in sync in eng/baselines.json as that file requires.

Tap and pointer now dispatch for real. Long press is the only gesture this backend can
detect but not raise: SendLongPressed and SendLongPressing are still internal, with no
ILongPressGestureController alternative. That is now stated as exactly two named members
rather than a blanket "tap, long-press and pointer are blocked".

Drag and drop also changed reason. Their dispatch members are public as of 26426.4, so
they are no longer dispatch-blocked - they remain unsupported purely because NUI has no
view-level drag/drop that maps onto the per-view recognizer semantics. The matrix now
says so.

Position resolution
-------------------
The new members take Func<IElement?, Point?> rather than a point, so MAUI can ask for a
position relative to an arbitrary element. The Tizen detectors report a view-local
position, which is returned for the view itself and for the null request. For any other
element the resolver returns null - MAUI's own encoding of "cannot be determined" -
because translating between two elements needs both on-screen origins, which requires a
native call per element that the Tizen platform layer does not expose here. A
plausible-looking wrong coordinate would be worse.

PlatformPointerEventArgs is left null and ButtonsMask at its default. Both are optional,
and NUI reports neither for touch and hover, so fabricating them would mislead.

Testing
-------
158 tests, up from 150. The dispatcher tests use real recognizers, so they prove the
public path delivers events rather than merely compiling; verified by stubbing the
SendTapped call out and watching three tests fail.

LongPressSendMembersAreStillInternalUpstream asserts by reflection that the two named
members are absent from BindingFlags.Public and present under NonPublic, so the support
matrix's central claim cannot rot. It fails when upstream opens the API.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Foundation landed the same MAUI 11.0.0-preview.7.26426.4 bump this branch had taken
ahead of it, so both conflicts were this branch's temporary pin against foundation's
official one. Foundation's version is taken verbatim for both Directory.Packages.props
and eng/baselines.json - they own those files, and the branch now carries no delta in
either.

The gesture work built on that pin is unaffected: same package stamp, same public
Tap/Pointer APIs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

Gesture dispatch gap has largely closed. Rebased on core 2610231 (MAUI 26426.4); PR is MERGEABLE / CLEAN.

Remote head: 8c18234 · CI: run 33013760085 — all 3 jobs pass, pull_request triggered · Tests: 158 (was 150)

Updated support matrix

Measured by reflecting over the shipped assemblies at each stamp, not by reading source:

26418.3 26426.4
TapGestureRecognizer internal SendTapped public
PointerGestureRecognizer internal all 5 send members public
DragGestureRecognizer / DropGestureRecognizer partly internal all send members public
LongPressGestureRecognizer internal still internal
Recognizer Detection Dispatch End to end
Pan / Pinch / Swipe ✅ controllers
Tap SendTapped
Pointer ✅ 5 send members
Long press ❌ internal ⚠️
Drag / Drop ❌ no NUI equivalent ✅ (now public)

The one remaining gap — exactly two members

LongPressGestureRecognizer.SendLongPressed(View, Func<IElement?, Point?>)
LongPressGestureRecognizer.SendLongPressing(View, GestureStatus, Func<IElement?, Point?>)

Absent from BindingFlags.Public, present under NonPublic; no ILongPressGestureController either. Making these public — exactly as #37420 did for tap and pointer — is sufficient; no new interface needed.

LongPressSendMembersAreStillInternalUpstream asserts this by reflection, so the matrix's central claim can't rot and fails the moment upstream opens the API. No duplicate upstream work opened, per instruction.

Drag/drop changed reason, not status. Dispatch is now public; they remain unsupported purely because NUI has no view-level drag/drop mapping onto per-view recognizer semantics. The matrix says so explicitly.

Position resolution

The new members take Func<IElement?, Point?> so MAUI can request a position relative to an arbitrary element. Tizen detectors report a view-local position, returned for the view itself and the null request. For any other element the resolver returns null — MAUI's own encoding of "cannot be determined" — because translating between two elements needs both on-screen origins, a native call the Tizen platform layer doesn't expose here. A plausible-looking wrong coordinate would be worse. PlatformPointerEventArgs/ButtonsMask are left at their optional defaults since NUI reports neither for touch/hover.

Also in this push

  • Retired eng/verify-nui-sources.sh in favour of tests/Maui.Tizen.Controls.RefPackCompile, a sibling of the Core lane using the pinned Samsung.Tizen.Ref.API15 via PackageDownload. Drops a network dependency and a second ref-pack version. Verified it still catches what the script caught: reintroducing the SetMinimumHoldingTime call fails with CS1061 on API15, removing it passes.
  • Zero foundation-owned deltas. I'd bumped the package pin ahead of foundation to unblock this work; foundation then landed the identical bump, so I took theirs verbatim for both Directory.Packages.props and eng/baselines.json. Both now match core byte for byte.

Diff is 40 files, all within this slice. Modal #37853 remains provisional with its expiry guard intact.

Dispatcher tests use real recognizers — verified load-bearing by stubbing out the SendTapped call and watching three fail.

Redth and others added 3 commits August 26, 2026 19:22
1. Position contract. MAUI documents GetPosition(relativeTo) as "the element to use
   as the coordinate reference, or null for SCREEN coordinates". The dispatcher was
   answering null with the view-local position, which is silently wrong. Gesture
   events now carry both spaces (TizenGesturePosition) and resolve null -> screen,
   same view -> local, other element -> null. The NUI detectors populate screen
   coordinates from TapGesture.ScreenPoint, LongPressGesture.ScreenPoint,
   PanGesture.ScreenPosition, PinchGesture.ScreenCenterPoint and
   Touch/Hover.GetScreenPosition. A missing screen coordinate stays null rather than
   being faked from the local one.

2. Real pixel scaler. AddTizenNuiControlsPlatform now registers a scaler backed by
   DeviceInfo.ScalingFactor. Identity scaling is only correct on a 1x display; Tizen
   wearables and TVs are not, so every pan, swipe, pinch, tap and pointer coordinate
   was wrong by the display factor. The registration is exposed as AddTizenPixelScaler
   taking a Func<double>, which is what makes it executable on the host: only reading
   the factor needs a device, and that atom is parameterized rather than baked in. The
   factor is read lazily because DeviceInfo is unusable until the app has initialised,
   and a non-positive or non-finite value degrades to 1 rather than throwing during
   window creation.

3. Unsubscribe is detach-only. MAUI calls Unsubscribe on ordinary page churn, not only
   at teardown, so dismissing dialogs there cancelled a DisplayAlertAsync the app was
   legitimately awaiting across a page swap. Dialogs are now dismissed only in Dispose,
   which the container calls at window-scope teardown.

4. Late-bound window. The subscription captured PlatformWindow at construction. MAUI
   can create the page handler - and therefore call Subscribe - before the window
   handler attaches the native window, so that snapshot could be null forever and
   silently drop every alert for the window's lifetime. The window is now resolved per
   request, and an unattached window services the request rather than discarding it.

5. Button masks. Tap and pointer dispatch now carry the originating button and filter
   against recognizer.Buttons, so a recognizer configured for Primary never fires on a
   right-click. Buttons come from Touch.GetMouseButton; Tizen.NUI.Hover exposes no
   equivalent, so hovers report none. Touch input reports MouseButton.Invalid, which
   maps to Primary - as does anything unclassified, so a stray value can never
   fabricate a secondary click.

6. Awaited stack operations. TizenModalHost discarded the PushAsync/PopAsync tasks,
   swallowing faults and letting a dialog open over a stack that had not taken the
   placeholder. Both are awaited, and ShownBehindPage is unwound even when the push
   faults.

7. Cross-window page reuse. A page popped from one window and pushed modally on another
   kept a handler bound to the originating window's IMauiContext, realizing it into the
   wrong view tree. Such handlers are now disconnected and rebuilt from the target
   window's factory, and the target context is applied unconditionally.

Testing
-------
193 tests, up from 158. Each fix was verified load-bearing by reverting it and
confirming the new tests fail: 9 failures across all seven areas, no overlap.

Two API constraints found by the ref-pack lane rather than at runtime:
TapGestureRecognizer.SendTapped takes no button argument, so the mask is enforced by
filtering; and Tizen.NUI.Hover has no GetMouseButton.

Long press remains the one gesture that cannot be dispatched, and the provisional
dotnet/maui#37853 modal contracts and their expiry guard are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
No conflicts. Full workload-free lane verified green on the merged tree, including
the ref-pack compile lanes and all three test projects.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Conflict was the workload-free lane's project list again, purely additive:
core's foundation-owned probes plus this branch's two Controls projects.

Also closes the last outstanding review item. TizenModalHost set
ShownBehindPage to open the dialog placeholder and then forced it back to
false, rather than restoring what it had been. ShownBehindPage is stack-wide
state belonging to whatever is already presented, so forcing false silently
reconfigured how every later push rendered for the lifetime of the window.
It is now saved and restored, including when the placeholder push faults.

Verified load-bearing: reverting the restore fails
ShownBehindPageIsRestoredRatherThanForcedFalse and
ShownBehindPageIsRestoredEvenWhenThePlaceholderPushFails.

196 tests. Long press remains internal and dotnet/maui#37853 remains open at
the pinned 11.0.0-preview.7.26426.4, both re-checked against the resolved
assembly this turn, so the provisional modal contracts and the long-press
lane stay exactly as they are.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

All review findings closed. Rebased on core 4e256f1; MERGEABLE / CLEAN.

Head: c4bc396 · CI: run 33026278181 — 3/3 pass, pull_request · Tests: 196 (was 158) · Diff: 41 files, all in-slice

Findings closed

# Finding Resolution
1 GetPosition(null) returned local, not screen TizenGesturePosition carries both spaces; resolves null→screen, same view→local, other→null
2 Identity scaler wrong on TV/wearable AddTizenPixelScaler wired to DeviceInfo.ScalingFactor, read lazily
3 Unsubscribe cancelled in-flight dialogs Detach-only; dismissal moved to Dispose at window-scope teardown
4 Eager window snapshot dropped alerts Window resolved per request; unattached window services rather than discards
5 No button-mask filtering Native button propagated and filtered against recognizer.Buttons
6 Stack push/pop not awaited Both awaited; faults propagate, placeholder always unwinds
7 Cross-window handler reuse Handlers bound to another MauiContext disconnected and rebuilt from target factory
8 Animation/stack metadata lost ShownBehindPage saved and restored instead of forced false

Every fix verified load-bearing by reverting it and confirming the new tests fail — 9 failures across findings 1–7, 2 more for finding 8, no overlap. Tests assert behaviour, not implementation shape.

Two API constraints found by the API15 lane, not at runtime

  • TapGestureRecognizer.SendTapped takes no button argument — it derives TappedEventArgs.Buttons from its own property — so the mask is enforced by filtering before dispatch.
  • Tizen.NUI.Hover has no GetMouseButton. A hover is pointer movement with nothing pressed, so hover transitions report no button. Touch's MouseButton.Invalid and anything unclassified map to Primary, so a stray value can never fabricate a right-click.

Provisional seams deliberately retained

Both re-verified this turn against the resolved assembly at the pinned 26426.4, not against docs:

Per instruction these are adopted only once available. Both have self-announcing guards that fail the build the moment upstream lands, so neither can outlive its purpose.

Note on a red lane I hit locally

Tizen.UIExtensions.NUI 0.9.2 briefly failed to restore with NU1100 — on pristine core too, and at 3c18089 which CI had already proven green. That ruled out a code regression: it was a stale negative NuGet HTTP-cache entry. dotnet nuget locals http-cache --clear fixed it. Flagging in case it surfaces on another machine; nothing to change in the repo.

Requesting final review.

Merge conflicts were the solution's test folder and the workload-free lane's
project list. Core reworded its RefPackCompile commentary and split the sample
into its own lane, so core's text was taken verbatim and this branch's two
Controls entries re-added around it.

Long-press mapping
------------------
dotnet/maui#37861 makes SendLongPressed and SendLongPressing public. It is
code/CI complete but NOT merged, so it is absent from the pinned package and is
not adopted here. What is landed is the translation that adoption will need,
specified and tested now so it is a small change later rather than a fresh
translation written at the time.

The in-box Tizen handler is deliberately NOT the model. Checked against
dotnet/maui net11.0: LongPressGestureHandler.cs has no Continuing branch at all,
so a Tizen long press never reports GestureStatus.Running and an app tracking
the gesture sees Started jump straight to Completed. iOS maps its equivalent
(UIGestureRecognizerState.Changed) to Running - verified in
GesturePlatformManager.iOS.cs - and that is what this backend follows:

  Started    -> Started    LongPressing
  Continuing -> Running    LongPressing
  Finished   -> Completed  LongPressed FIRST, then LongPressing
  Cancelled  -> Canceled   LongPressing only; never LongPressed, never Command

A canceled press reports a status change but is not a press, so raising
LongPressed or running the command there would fire the app's handler for a
gesture the user aborted. CompletesLongPress encodes that and is tested per
state.

The handler previously dropped Continuing, inheriting the in-box gap. It now
passes it through, and ToLongPressStatus/CompletesLongPress encode the table
above. Reintroducing the gap fails four tests across both the mapping and the
handler's ordering, so it cannot come back silently during adoption.

208 tests, up from 196. The API15 compile lane passes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

Long-press state machine specified. Rebased on core dbcf82e; MERGEABLE / CLEAN.

Head: 69ffc44 · CI: run 33034912965 3/3 pass · Tests: 208 (was 196) · Diff: 41 files, all in-slice

Long press — mapping landed, adoption still waiting

dotnet/maui#37861 is OPEN, so it is absent from the pinned 26426.4 package and is not adopted. What is landed is the translation adoption will need, specified and tested now:

Native Tizen GestureStatus Events
Started Started LongPressing
Continuing Running LongPressing
Finished Completed LongPressed first, then LongPressing
Cancelled Canceled LongPressing only — never LongPressed, never the command

The in-box Tizen handler is deliberately not the model. Verified against dotnet/maui net11.0: LongPressGestureHandler.cs has no Continuing branch at all, so a Tizen long press never reports Running and an app tracking the gesture sees Started jump straight to Completed. GesturePlatformManager.iOS.cs maps UIGestureRecognizerState.ChangedRunning, and that is what this backend follows.

My handler had inherited that gap — it dropped Continuing. Fixed, plus ToLongPressStatus/CompletesLongPress encode the table above. Reintroducing the gap fails 4 tests across both the mapping and the handler's ordering, so it cannot come back silently during adoption.

CompletesLongPress is separate on purpose: a canceled press reports a status change but is not a press, so raising LongPressed or running the Command there would fire the app's handler for a gesture the user aborted.

New behavioural coverage: full ordered sequence incl. two Running updates, cancel path asserting Finished never appears, no-meaning states ignored, and per-state mapping theories. API15 compile lane passes.

⚠️ Pre-existing flaky test in core — not from this PR

CI first went red on Maui.Tizen.Core.UnitTestsDispatcherTests.ConcurrentDelayedDispatchesEachFireExactlyOnce. Evidence it is core-owned and flaky, not caused by this branch:

  1. My diff touches zero files under Maui.Tizen.Core — verified.
  2. Core's own CI at dbcf82e — the exact commit merged here — fails on the identical test with the identical 1 failed / 279 passed signature.
  3. Core's recent history alternates pass/fail across commits, the signature of flakiness.
  4. Rerunning my job with no code change turned it green.

Root cause looks like wall-clock sensitivity: the test schedules 8 delayed dispatches at 5–12 ms and drains for 600 ms, asserting all 8 fired. On a loaded agent that window is not reliably sufficient — items 2–7 reported 0. Worth making the drain deadline-based rather than a fixed budget. Flagging rather than fixing, since it is outside this PR's scope.

Ready for final review.

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.

1 participant