Skip to content

Commit e9e24e5

Browse files
authored
Merge pull request #5477 from harder/fix-5360-cull-occluded-subviews
Fixes #5360. Cull fully occluded overlapped opaque subviews during draw
2 parents d939953 + 21d875a commit e9e24e5

2 files changed

Lines changed: 569 additions & 1 deletion

File tree

Terminal.Gui/ViewBase/View.Drawing.cs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)