Tizen alerts, modal navigation and gesture infrastructure - #9
Conversation
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>
|
CI is green on this branch. Repository CI only triggers on PRs targeting The Something for the foundation to consider separately: extending the |
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>
|
Rebased onto the core vertical slice and retargeted; CI green. Base: now Merged rather than rebased, deliberately. 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- |
…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>
|
Merged core Head: 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 — Nothing from this branch was preserved by preference. In particular my grep-based workload probe from The controls platform tests now also sit inside core's Diff against the new base is still exactly 40 files, all within this slice. Test count unchanged at 150. |
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>
|
Gesture dispatch gap has largely closed. Rebased on core Remote head: Updated support matrixMeasured by reflecting over the shipped assemblies at each stamp, not by reading source:
The one remaining gap — exactly two membersAbsent from
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 resolutionThe new members take Also in this push
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 |
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>
|
All review findings closed. Rebased on core Head: Findings closed
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
Provisional seams deliberately retainedBoth re-verified this turn against the resolved assembly at the pinned
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
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>
|
Long-press state machine specified. Rebased on core Head: Long press — mapping landed, adoption still waitingdotnet/maui#37861 is OPEN, so it is absent from the pinned
The in-box Tizen handler is deliberately not the model. Verified against dotnet/maui My handler had inherited that gap — it dropped
New behavioural coverage: full ordered sequence incl. two
|
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) andIGesturePlatformManager/IGesturePlatformManagerFactory(#36655) — plus a provisional alignment with the still-open modal seam (#37853). NoDispatchProxy, no private reflection, noInternalsVisibleTo.150 host-side tests, all green. Diff is 40 files, all within this slice.
Alerts and dialog coordination
TizenAlertManagerimplementsIAlertManager;TizenAlertManagerSubscriptionimplementsIAlertManagerSubscription. Both port the NUIAlertRequestHelper. Registered scoped, because MAUI resolvesIAlertManagerfrom 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
Unsubscribeas "drop the reference", which on Tizen leaves an orphaned modal popup on screen and the awaitingDisplayAlertAsynccaller pending forever.TizenAlertRegistrationMode.SubscriptionOnlygives 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:
Page.IsBusyscopes 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.Modal page navigation —⚠️ provisional
This was the one area that could not be built on the shipped .NET MAUI 11 surface.
ModalNavigationManager.Tizen.csupstream is an internal partial-class completion compiled intoMicrosoft.Maui.Controls, and the neutralStandardpartial 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.7package this repository builds against.Core/Platform/Modal/ProvisionalModalNavigationContracts.cscarries 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, notMicrosoft.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.ProvisionalModalNavigationContractTestskeeps the copies honest — it asserts each interface's member shape against the PR, asserts the namespace rule, and fails outright onceMicrosoft.Maui.Controls.Platform.IModalNavigationPlatformappears in the referenced assembly, with instructions to delete the provisional file. The copies cannot drift and cannot outlive their purpose.TizenModalNavigationPlatformportsModalNavigationManager.Tizen.csonto the seam.SendDisappearing/SendAppearingand the manual_platformModalPagesbookkeeping 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.TizenModalPageRealizerreplacesmodal.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:
NuiModalHostis replaced byTizenModalHostdriving the newITizenNavigationStack. 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
TizenGesturePlatformManagerFactoryimplementsIGesturePlatformManagerFactoryand works with anyIViewHandlervia the standardPlatformView/ContainerViewpair. The dependency on MAUI's Tizen-onlyIPlatformViewHandlershape 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 noPointerGestureRecognizersupport — derived from NUI touch and hover events. TheRegistrarassembly-scanning fallback is replaced by explicit DI seams.MAUI 11 exposes public gesture controller interfaces for pan, pinch and swipe only.
TapGestureRecognizer.SendTapped, theLongPressGestureRecognizersend members and the entirePointerGestureRecognizersend 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 aTapGestureRecognizerbehaves 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.cscallsTizen.NUI.LongPressGestureDetector.SetMinimumHoldingTime, which does not exist in TizenFX (checked againstSamsung.Tizen.RefAPI13 and API15). That source has not compiled since Tizen left the MAUI build.MinimumPressDurationis 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 inDirectory.Build.propsis preserved andNoProjectFallsBackToANeutralTargetFrameworkstill passes.eng/verify-nui-sources.shtype-checks the NUI half against TizenFX reference assemblies without the Samsung workload. It has caught two real problems: theSetMinimumHoldingTimecall above, and a back-button implementation that reached forSetBackButtonPressedHandler— 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.Microsoft.Maui.Platform.WindowExtensionsand is consumed byMauiApplication, 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.NuiAlertDialogFactorycarries a private keyboard mapping duplicatingMicrosoft.Maui.Platform.KeyboardExtensions.ToPlatform. Swap to the Core one onceMaui.Tizen.Coreexposes 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.shmatched any workload id containing "tizen", so a machine with MAUI's ownmaui-tizenworkload was reported as having the Samsung platform SDK. It does not — such a machine still failsNETSDK1139. Left as-is this would have promoted the Tizen lane to required and broken CI. Detection now mirrors the manifest probe inDirectory.Build.props.Not included
BlazorWebView — owned by another session.