Visitar URL original
fix(imaging): Keep a brush off the source it left by mikernet · Pull Request #26370 · unoplatform/uno · GitHub
Skip to content

fix(imaging): Keep a brush off the source it left - #26370

Open
mikernet wants to merge 1 commit into
unoplatform:masterfrom
mikernet:dev/mikernet/imagebrush-source-switch
Open

mikernet wants to merge 1 commit into
unoplatform:masterfrom
mikernet:dev/mikernet/imagebrush-source-switch

Conversation

@mikernet

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #26368

PR Type:

🐞 Bugfix

What changed? 🚀

When its ImageSource changed, an ImageBrush subscribed to the new source before it disposed its subscription to the old one (#26368), since 0015c9b (5.0); before that, it disposed it first. A new source that has its image already, or opens synchronously (a WriteableBitmap), delivers it while the brush subscribes, and the brush raises ImageOpened then. If that handler, or other app code run meanwhile, made the old source open again synchronously (for example oldWriteableBitmap.Invalidate()), the old subscription was still active: the brush took the old image and drew it for the new source.

  • Old source: the brush now disposes its subscription before it subscribes to the new source, as Image does (ReleaseSubscription) and as WinUI does: CImageBrush::SetValue calls RemoveCallback on the old source before it sets the new one (imagebrush.cpp).
  • Replaced source: a subscription can't always be disposed before app code runs. When the brush's ImageOpened handler, raised while it subscribes, switches it to a third source, the subscription to the replaced source is only disposed once that first subscribe returns, and a synchronous open of the replaced source in between reached the brush too. Each subscription's callback now carries the version of the brush's source it was made for (the _subscriptionVersion the brush already keeps for that case), and the brush ignores deliveries for a version it has left.

Once disposed, a subscription is never notified: a source's asynchronous open, including a completed bitmap-cache entry, whose delivery is queued on the dispatcher, skips disposed subscriptions. So the bug needs app code that reopens a source synchronously while the brush switches. A brush whose BitmapImage.UriSource changes keeps drawing the previous image until the new one opens; that is unchanged and matches WinUI, which keeps the surface while it switches URIs (CImageSource::ReloadImage).

Image already disposed its old subscription first, and doesn't need the version check: the sources that run app code while an Image subscribes (BitmapImage, SvgImageSource) deliver through the dispatcher, after the Image has disposed the subscription, and the ones that open synchronously (WriteableBitmap, RenderTargetBitmap) raise no events.

Tests

In Given_Image_Release (Skia):

  • When_Brush_Open_Handler_Reopens_Previous_Source_Then_Brush_Draws_New_One: a brush switched between two WriteableBitmaps, whose ImageOpened handler invalidates the one it left, draws the new one.
  • When_Brush_Open_Handler_Switches_Source_Then_Reopened_Replaced_Source_Not_Drawn: a brush whose ImageOpened handler switches it to a third WriteableBitmap and then invalidates the one it replaced draws the third one.

Both fail without the change. With only the old subscription disposed first, the second test still fails. The version check alone would also make the first test pass, since it ignores the old source's delivery; disposing first keeps the old source from notifying the brush at all, as on WinUI.

Runs (Skia desktop): Given_ImageBrush, Given_Image_Release, Given_Image, Given_WriteableBitmap, Given_BitmapSource and Given_RelativeTransform: 128 passed, 5 skipped and 1 failed. The failure, When_WriteableBitmap_Assigned_With_Data_Present, also fails with master's code on this machine, which runs at 125% display scaling (see #25633).

Runs (Skia WebAssembly): Given_Image_Release and Given_ImageBrush: 45 passed (the two new tests among them) and 26 desktop-only tests skipped.

PR Checklist ✅

When its ImageSource changed, an ImageBrush subscribed to the new source
before it disposed its subscription to the old one. A new source that has
its image, or opens synchronously (a WriteableBitmap), delivers it while
the brush subscribes, and the brush raises ImageOpened then. A handler
that made the old source open again synchronously, such as
oldBitmap.Invalidate(), still reached the brush, which then drew the old
image for the new source.

The brush now disposes the old subscription first, as Image does and as
WinUI removes its callback from the old source first (CImageBrush::SetValue).

A source that the brush's ImageOpened handler replaced while the brush
subscribed to it keeps its subscription until that subscribe returns, and
a synchronous open of it in between reached the brush too. Each
subscription's callback now carries the source version it was made for,
and the brush ignores deliveries for a version it has left.

Fixes unoplatform#26368
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@mergify

mergify Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue). If it does not match the queue conditions yet, it enters the queue once it does.

  • Queue this pull request

mikernet added a commit to Singulink/uno that referenced this pull request Nov 13, 2026
Port of unoplatform#26370 commit f340a71 (issue
unoplatform#26368).

When its ImageSource changed, an ImageBrush subscribed to the new source
before it disposed its subscription to the old one. A new source that
has its image, or opens synchronously, delivers it while the brush
subscribes, and the brush raises ImageOpened then: a handler that made
the old source open again synchronously still reached the brush, which
drew the old image for the new source. The brush now disposes the old
subscription first, as WinUI does (CImageBrush::SetValue), and each
subscription's callback carries the source version it was made for, so
a source the handler replaced during the subscribe is ignored too.
This build's SVG ImageOpened handling is unchanged.
mikernet added a commit to Singulink/uno that referenced this pull request Nov 14, 2026
Takes Uno's servicing/6.8 from 6951c2c up to 5254ace (42 pull
requests). Conflicts, by area:

IME and TextBox (unoplatform#25984, backport of unoplatform#25804; unoplatform#25952, backport of unoplatform#25619;
unoplatform#25976, backport of unoplatform#25818)
- Takes upstream's composition design (unoplatform#25804): the browser's input is
  the source of truth for the text, compositions included. This drops
  the composition parts of the unoplatform#24502 port (90746b8): its
  TypeScript composition handling, the StartIndex/TextChangePending
  event arguments, the IsReadOnly forwarding to the hidden input, and
  its Given_TextBox.BrowserSoftKeyboard tests.
- Keeps the non-IME browser fixes on top of it, in the form their
  master pull requests have on unoplatform#25804: Enter raised once per press and
  tracked by key code (unoplatform#25174, with Given_TextBox.BrowserEnterKey from
  master in place of the Enter tests that lived in the dropped file),
  the iOS refocus on tap (unoplatform#25185), pastes without text and isIOS from
  BrowserInputHelper.
- Moves the unoplatform#26005 MaxLength fixes onto unoplatform#25804's native input path, as
  master has them: a refused browser edit keeps the TextBox's selection,
  TextBoxView.UpdateTextFromNative reports whether the edit went in, and
  the IME keeps its range on the applied text (master be85699, which
  d226897 had ported onto the unoplatform#24502 IME). Adds that commit's browser
  and platform-applied IME tests.
- TextBox.pointers.skia.cs: upstream's file (unoplatform#25619 iOS touch editing,
  unoplatform#25818, unoplatform#25804), with the unoplatform#25126 port (the hold acts when it starts,
  a touch right-tap keeps the caret) in the form master's unoplatform#25126 branch
  has it on unoplatform#25619, including focusing before the iOS hold timer acts
  and its iOS not-focusable test.

Pointers (unoplatform#25976, backport of unoplatform#25818)
- Tapped and RightTapped are raised once the PointerReleased bubbled
  (unoplatform#25818), with the unoplatform#25732 port's window coordinates (2-argument
  routed event args, RawPosition for ContextRequested), as master's
  unoplatform#25732 branch has them.
- The release wrapper of unoplatform#25818 keeps the unoplatform#25126 port's
  StopContextMenuTimer at the start of the release.

DependencyObject (unoplatform#26142, backport of unoplatform#26123)
- Takes upstream, then the code of unoplatform#25839 (97a2f17), which replaces
  this branch's unoplatform#25102 ports (800247f, be41da6, fd40bf9,
  7546379). DependencyObjectStore.Binder.cs, .alc.cs,
  Application.Alc.cs, ResourceDictionary.alc.cs and their tests now
  match unoplatform#25839; this drops 800247f's own
  SingleParentNonFE_Parent_Removes_Value_Then_Next_Parent_Starts_Clean
  test, which unoplatform#25839 doesn't have.

Imaging (unoplatform#25597, unoplatform#25915, unoplatform#25911, unoplatform#25897)
- Keeps this branch's side: it has the later forms of these fixes
  (unoplatform#25124, unoplatform#25559, unoplatform#25633 and unoplatform#26370 ports), where upstream has the
  6.8-only unoplatform#25597 design and earlier snapshots. Upstream's shared
  download, IgnoreImageCache and failed-load fixes are covered by
  SharedImageDecode. Adds upstream's When_Source_Outlives_Brushes test.

ToolTip (unoplatform#26098, backport of unoplatform#25137 and unoplatform#26096)
- Keeps this branch's unoplatform#25137 port and adds unoplatform#26096 in master's form: the
  service releases only a ToolTip it opened, and leaves one the app
  opened during the show delay to the app. Keeps both sides' tests.

Flyout (unoplatform#26062, backport of unoplatform#26060)
- This branch had the same commits already; keeps the "Keep
  non-resizable flyouts in window" port and its tests.

Also removes a helper pair in Given_AccessibleTextBox that both sides
added.
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.

[Skia] ImageBrush draws its previous ImageSource when that source opens again while the brush switches

1 participant