@@ -660,10 +660,47 @@ public void DrawSubViews (DrawContext? context = null)
660660 // SubViews earlier in the collection are drawn last (on top).
661661 // NOTE: Do not use SubViews or GetSubViews() here as GetSubViews can be overridden to return a different set of views or ordering.
662662 // NOTE: We need to draw exactly the views in InternalSubViews.
663- foreach ( View view in InternalSubViews . Snapshot ( ) . Where ( v => v . Visible ) . Reverse ( ) )
663+ View [ ] drawOrder = InternalSubViews . Snapshot ( ) . Where ( v => v . Visible ) . Reverse ( ) . ToArray ( ) ;
664+
665+ // Occlusion culling (issue #5360): when Overlapped opaque siblings stack (e.g. Tabs pages,
666+ // overlapped Windows), a lower-Z sibling that is fully covered by the higher-Z opaque peers
667+ // already drawn produces no visible output — every cell it would draw, including its own
668+ // RenderLineCanvas, is clipped away by the clip "holes" those peers punched in DoDrawComplete.
669+ // Skipping such a sibling's Draw is therefore output-neutral. opaqueCoverage accumulates the
670+ // screen region opaquely covered by higher-Z peers (iterated highest-Z first). The whole
671+ // check is gated on at least one Overlapped sibling so the common (non-overlapping) path is
672+ // zero-overhead.
673+ bool considerOcclusion = Driver is { } && drawOrder . Length > 1 && drawOrder . Any ( v => v . Arrangement . FastHasFlags ( ViewArrangement . Overlapped ) ) ;
674+ Region ? opaqueCoverage = null ;
675+
676+ foreach ( View view in drawOrder )
664677 {
678+ Rectangle coveredScreenRect = Rectangle . Empty ;
679+ bool opaque = considerOcclusion && view . IsOpaqueForOcclusion ( out coveredScreenRect ) ;
680+
681+ if ( opaque
682+ && view . Arrangement . FastHasFlags ( ViewArrangement . Overlapped )
683+ && ! view . SuperViewRendersLineCanvas
684+ && view . ShadowStyle is null or ShadowStyles . None
685+ && ! view . ParticipatesInTransparentMouseHitTesting ( )
686+ && IsFullyCovered ( view . FrameToScreen ( ) , opaqueCoverage ) )
687+ {
688+ // Fully occluded by higher-Z opaque peers. Clear the draw flags to mirror a
689+ // drawn-but-fully-clipped pass (Draw() would have called ClearNeedsDraw at its end);
690+ // otherwise this view would stay dirty forever and keep the SuperView redrawing.
691+ view . ClearNeedsDraw ( ) ;
692+
693+ continue ;
694+ }
695+
665696 view . Draw ( context ) ;
666697
698+ if ( opaque )
699+ {
700+ opaqueCoverage ??= new ( ) ;
701+ opaqueCoverage . Union ( coveredScreenRect ) ;
702+ }
703+
667704 if ( ! view . SuperViewRendersLineCanvas )
668705 {
669706 continue ;
@@ -697,6 +734,113 @@ public void DrawSubViews (DrawContext? context = null)
697734 _pendingOverlappedCellMaps = overlappedCellMaps ;
698735 }
699736
737+ /// <summary>
738+ /// Determines whether this view opaquely covers a screen rectangle — such that nothing drawn behind
739+ /// that rectangle can show through it. Used by <see cref="DrawSubViews"/> occlusion culling
740+ /// (issue #5360).
741+ /// </summary>
742+ /// <remarks>
743+ /// <para>
744+ /// The view's content is opaque only when its content layer, <see cref="Border"/>, and
745+ /// <see cref="Padding"/> are not <see cref="ViewportSettingsFlags.Transparent"/>. The
746+ /// <see cref="Margin"/> is transparent by default; when it is, the opaquely-covered region is the
747+ /// area inside the Margin (<see cref="Border"/>'s frame). When the Margin is explicitly opaque,
748+ /// the full <see cref="Frame"/> is covered.
749+ /// </para>
750+ /// <para>
751+ /// This is the same notion of "opaque" used by <see cref="DoDrawComplete"/> to exclude a view's
752+ /// drawn area from the clip; here it is the area a higher-Z peer hides from the views drawn after
753+ /// it.
754+ /// </para>
755+ /// </remarks>
756+ /// <param name="coveredScreenRect">
757+ /// When this returns <see langword="true"/>, the screen-relative rectangle this view covers opaquely.
758+ /// <see cref="Rectangle.Empty"/> otherwise.
759+ /// </param>
760+ /// <returns><see langword="true"/> if the view opaquely covers <paramref name="coveredScreenRect"/>; otherwise <see langword="false"/>.</returns>
761+ private bool IsOpaqueForOcclusion ( out Rectangle coveredScreenRect )
762+ {
763+ coveredScreenRect = Rectangle . Empty ;
764+
765+ if ( ViewportSettings . FastHasFlags ( ViewportSettingsFlags . Transparent )
766+ || Border . ViewportSettings . FastHasFlags ( ViewportSettingsFlags . Transparent )
767+ || Padding . ViewportSettings . FastHasFlags ( ViewportSettingsFlags . Transparent ) )
768+ {
769+ return false ;
770+ }
771+
772+ bool marginTransparent = Margin . ViewportSettings . FastHasFlags ( ViewportSettingsFlags . Transparent ) ;
773+ coveredScreenRect = marginTransparent ? Border . FrameToScreen ( ) : Margin . FrameToScreen ( ) ;
774+
775+ return true ;
776+ }
777+
778+ /// <summary>
779+ /// Determines whether culling this view would diverge mouse hit-testing state, so the view must not be
780+ /// culled by <see cref="DrawSubViews"/> occlusion culling (issue #5360).
781+ /// </summary>
782+ /// <remarks>
783+ /// <para>
784+ /// The cull path skips <see cref="Draw(DrawContext?)"/>, hence <see cref="DoDrawComplete"/>, which
785+ /// repopulates the <see cref="CachedDrawnRegion"/> that <see cref="GetViewsUnderLocation"/> consults
786+ /// for <see cref="ViewportSettingsFlags.TransparentMouse"/> layers. A view's own
787+ /// <see cref="CachedDrawnRegion"/> is invalidated by <see cref="SetNeedsDraw()"/>, so a
788+ /// <see cref="ViewportSettingsFlags.TransparentMouse"/> view that is culled would be left with a null
789+ /// cache and dropped from hit-testing.
790+ /// </para>
791+ /// <para>
792+ /// Adornment caches survive <see cref="SetNeedsDraw()"/>, but hit-testing only consults them when the
793+ /// adornment is <see cref="ViewportSettingsFlags.TransparentMouse"/> with non-empty
794+ /// <see cref="AdornmentImpl.Thickness"/>; those are excluded too so the first draw cannot leave a thick
795+ /// transparent-mouse adornment without a cache. <see cref="Margin"/> is
796+ /// <see cref="ViewportSettingsFlags.TransparentMouse"/> by default, so this only excludes views with an
797+ /// actual Margin thickness — the common (empty-margin) overlapped view is still culled.
798+ /// </para>
799+ /// </remarks>
800+ /// <returns><see langword="true"/> if culling would diverge hit-testing; otherwise <see langword="false"/>.</returns>
801+ private bool ParticipatesInTransparentMouseHitTesting ( )
802+ {
803+ if ( ViewportSettings . FastHasFlags ( ViewportSettingsFlags . TransparentMouse ) )
804+ {
805+ return true ;
806+ }
807+
808+ return IsThickTransparentMouse ( Margin ) || IsThickTransparentMouse ( Border ) || IsThickTransparentMouse ( Padding ) ;
809+
810+ static bool IsThickTransparentMouse ( AdornmentImpl adornment ) =>
811+ adornment . Thickness != Thickness . Empty && adornment . ViewportSettings . FastHasFlags ( ViewportSettingsFlags . TransparentMouse ) ;
812+ }
813+
814+ /// <summary>
815+ /// Determines whether <paramref name="screenRect"/> is entirely contained within the region opaquely
816+ /// covered by higher-Z peers (<paramref name="opaqueCoverage"/>). Used by <see cref="DrawSubViews"/>
817+ /// occlusion culling (issue #5360).
818+ /// </summary>
819+ /// <param name="screenRect">The screen-relative rectangle to test.</param>
820+ /// <param name="opaqueCoverage">
821+ /// The accumulated screen region covered opaquely by higher-Z peers, or <see langword="null"/> if no
822+ /// opaque peer has been drawn yet.
823+ /// </param>
824+ /// <returns>
825+ /// <see langword="true"/> if <paramref name="screenRect"/> is non-empty and fully covered; otherwise
826+ /// <see langword="false"/>.
827+ /// </returns>
828+ private static bool IsFullyCovered ( Rectangle screenRect , Region ? opaqueCoverage )
829+ {
830+ if ( screenRect . IsEmpty || opaqueCoverage is null )
831+ {
832+ return false ;
833+ }
834+
835+ // Subtract the covered region from the candidate's frame; if nothing is left, the candidate is
836+ // fully covered. Using Region difference (rather than Region.Contains, which only tests a single
837+ // stored rectangle) correctly handles coverage spread across multiple opaque peers.
838+ Region remaining = new ( screenRect ) ;
839+ remaining . Exclude ( opaqueCoverage ) ;
840+
841+ return remaining . IsEmpty ( ) ;
842+ }
843+
700844 #endregion DrawSubViews
701845
702846 #region DrawLineCanvas
0 commit comments