Skip to content

Commit ca84cbf

Browse files
committed
Reuse image presenters for icon updates
1 parent 6537b77 commit ca84cbf

2 files changed

Lines changed: 176 additions & 5 deletions

File tree

src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconBox.cs

Lines changed: 175 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ public partial class IconBox : ContentControl
2929
private long _requestVersion;
3030
private IconRequestMeasurement _activeRequestDiagnostics;
3131
private IIconRequestDemand? _activeRequestDemand;
32+
33+
// ImageIconSource does not render through IconSourceElement. Reassigning Source on
34+
// one realized Image left recycled rows intermittently blank in testing. Keep a
35+
// stable Grid and alternate inactive Image slots so Source changes occur only on a
36+
// collapsed element before it is shown.
37+
private Grid? _imagePresenter;
38+
private Image? _firstImageSlot;
39+
private Image? _secondImageSlot;
40+
private Image? _activeImageSlot;
41+
private bool _imagePresenterDisabled;
3242
private long _diagnosticId;
3343
private IconRequestSite _derivedRequestSite;
3444
private bool _hasDerivedRequestSite;
@@ -429,11 +439,18 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
429439
switch (e.NewValue)
430440
{
431441
case null:
432-
self.Content = null;
442+
var imagePresenterWasActive = ReferenceEquals(self.Content, self._imagePresenter);
443+
self.ClearImagePresenter();
444+
if (!imagePresenterWasActive)
445+
{
446+
self.Content = null;
447+
}
448+
433449
self.Padding = default;
434450
break;
435451
case FontIconSource fontIcon:
436452
var fontElementStartedAt = IconLoadDiagnostics.BeginElementUpdate();
453+
self.ClearImagePresenter();
437454
self.UpdateLastFontSize();
438455
fontIcon.FontSize = self._lastFontSize;
439456
if (self.Content is IconSourceElement iconSourceElement)
@@ -452,6 +469,7 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
452469
break;
453470
case BitmapIconSource bitmapIcon:
454471
var bitmapElementStartedAt = IconLoadDiagnostics.BeginElementUpdate();
472+
self.ClearImagePresenter();
455473
if (self.Content is IconSourceElement iconSourceElement2)
456474
{
457475
iconSourceElement2.IconSource = bitmapIcon;
@@ -467,8 +485,16 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
467485

468486
break;
469487

488+
case ImageIconSource imageIcon:
489+
var imageElementStartedAt = IconLoadDiagnostics.BeginElementUpdate();
490+
var reusedImagePresenter = self.PresentImageIconSource(imageIcon);
491+
IconLoadDiagnostics.RecordElementUpdate(reusedImagePresenter, imageIcon, imageElementStartedAt);
492+
self.Padding = default;
493+
break;
494+
470495
case IconSource source:
471496
var sourceElementStartedAt = IconLoadDiagnostics.BeginElementUpdate();
497+
self.ClearImagePresenter();
472498
self.Content = source.CreateIconElement();
473499
IconLoadDiagnostics.RecordElementUpdate(reused: false, source, sourceElementStartedAt);
474500
self.Padding = default;
@@ -479,6 +505,134 @@ private static void OnSourcePropertyChanged(DependencyObject d, DependencyProper
479505
}
480506
}
481507

508+
private bool PresentImageIconSource(ImageIconSource source)
509+
{
510+
if (_imagePresenterDisabled)
511+
{
512+
Content = source.CreateIconElement();
513+
return false;
514+
}
515+
516+
if (source.ImageSource is null)
517+
{
518+
var imagePresenterWasActive = ReferenceEquals(Content, _imagePresenter);
519+
ClearImagePresenter();
520+
if (!imagePresenterWasActive)
521+
{
522+
Content = null;
523+
}
524+
525+
return _imagePresenter is not null;
526+
}
527+
528+
var reused = _imagePresenter is not null;
529+
530+
try
531+
{
532+
var presenter = EnsureImagePresenter();
533+
var nextSlot = ReferenceEquals(_activeImageSlot, _firstImageSlot)
534+
? EnsureSecondImageSlot()
535+
: _firstImageSlot!;
536+
var previousSlot = _activeImageSlot;
537+
538+
if (previousSlot is not null)
539+
{
540+
previousSlot.Visibility = Visibility.Collapsed;
541+
}
542+
543+
nextSlot.Source = source.ImageSource;
544+
nextSlot.Visibility = Visibility.Visible;
545+
546+
if (previousSlot is not null)
547+
{
548+
previousSlot.Source = null;
549+
}
550+
551+
_activeImageSlot = nextSlot;
552+
if (!ReferenceEquals(Content, presenter))
553+
{
554+
Content = presenter;
555+
}
556+
557+
return reused;
558+
}
559+
catch (Exception ex)
560+
{
561+
DisableImagePresenter();
562+
Logger.LogError($"Failed to update reusable image presenter ({GetDiagnosticDescription()})", ex);
563+
Content = source.CreateIconElement();
564+
return false;
565+
}
566+
}
567+
568+
private Grid EnsureImagePresenter()
569+
{
570+
if (_imagePresenter is not null)
571+
{
572+
return _imagePresenter;
573+
}
574+
575+
var presenter = new Grid
576+
{
577+
IsHitTestVisible = false,
578+
};
579+
var firstSlot = CreateImageSlot();
580+
presenter.Children.Add(firstSlot);
581+
582+
_imagePresenter = presenter;
583+
_firstImageSlot = firstSlot;
584+
return presenter;
585+
}
586+
587+
private Image EnsureSecondImageSlot()
588+
{
589+
if (_secondImageSlot is not null)
590+
{
591+
return _secondImageSlot;
592+
}
593+
594+
var secondSlot = CreateImageSlot();
595+
_imagePresenter!.Children.Add(secondSlot);
596+
_secondImageSlot = secondSlot;
597+
return secondSlot;
598+
}
599+
600+
private static Image CreateImageSlot()
601+
{
602+
return new Image
603+
{
604+
IsHitTestVisible = false,
605+
Stretch = Stretch.Uniform,
606+
Visibility = Visibility.Collapsed,
607+
};
608+
}
609+
610+
private void ClearImagePresenter()
611+
{
612+
_activeImageSlot?.Visibility = Visibility.Collapsed;
613+
_firstImageSlot?.Source = null;
614+
_secondImageSlot?.Source = null;
615+
_activeImageSlot = null;
616+
}
617+
618+
private void DisableImagePresenter()
619+
{
620+
try
621+
{
622+
ClearImagePresenter();
623+
}
624+
catch (Exception ex)
625+
{
626+
Logger.LogError($"Failed to clear reusable image presenter ({GetDiagnosticDescription()})", ex);
627+
}
628+
629+
_imagePresenter = null;
630+
_firstImageSlot = null;
631+
_secondImageSlot = null;
632+
_activeImageSlot = null;
633+
_imagePresenterDisabled = true;
634+
}
635+
482636
private static void OnFallbackSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
483637
{
484638
if (d is not IconBox self)
@@ -518,9 +672,15 @@ private static void OnSourceKeyPropertyChanged(DependencyObject d, DependencyPro
518672
return;
519673
}
520674

521-
// A recycled IconBox must stop presenting the preceding item's icon before
522-
// the replacement request has a chance to yield or enter the load queue.
523-
self.UpdatePresentedSource();
675+
// Keep the preceding source only while a replacement has a chance to resolve
676+
// synchronously in Refresh. RequestIconFromSource presents the fallback before
677+
// an asynchronous suspension, so the preceding item can never reach another frame.
678+
// If no request can start now, clear it immediately instead.
679+
if (!self.IsLoaded || self._sourceRequested is null)
680+
{
681+
self.UpdatePresentedSource();
682+
}
683+
524684
self.RequestRefresh(IconRequestReason.SourceChanged);
525685
}
526686

@@ -582,6 +742,17 @@ private static async void RequestIconFromSource(
582742

583743
if (requestVersion == iconBox._requestVersion)
584744
{
745+
// A synchronous provider failure occurs before the pending-source path
746+
// gets a chance to clear the preceding item.
747+
try
748+
{
749+
iconBox.UpdatePresentedSource();
750+
}
751+
catch (Exception presentationException)
752+
{
753+
Logger.LogError($"Failed to clear icon after a request failure ({iconBox.GetDiagnosticDescription()})", presentationException);
754+
}
755+
585756
// Do not dispatch immediately: a deterministic failure would recurse forever.
586757
// Keep the request pending for the next external lifecycle or source trigger.
587758
iconBox.MarkRefreshPending(IconRequestReason.Retry);

src/modules/cmdpal/Tests/Microsoft.CmdPal.UI.UnitTests/IconPresentationStateTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.CmdPal.UI.UnitTests;
1111
public class IconPresentationStateTests
1212
{
1313
[TestMethod]
14-
public void SourceChangeImmediatelyReplacesResolvedSourceWithPlacementFallback()
14+
public void SourceChangeResetsResolvedSourceToPlacementFallback()
1515
{
1616
var state = new IconPresentationState<string>
1717
{

0 commit comments

Comments
 (0)