drawnui-react 0.1.0-preview.2 → 0.1.0-preview.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +17 -4
  2. package/SKIPPED.md +33 -27
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +40 -0
  6. package/dist/controls/DrawnGame.js +64 -0
  7. package/dist/controls/GridStructure.d.ts +16 -2
  8. package/dist/controls/GridStructure.js +40 -4
  9. package/dist/controls/SkiaBackdrop.d.ts +8 -2
  10. package/dist/controls/SkiaBackdrop.js +48 -8
  11. package/dist/controls/SkiaButton.d.ts +19 -2
  12. package/dist/controls/SkiaButton.js +41 -11
  13. package/dist/controls/SkiaCarousel.d.ts +9 -1
  14. package/dist/controls/SkiaCarousel.js +16 -3
  15. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  16. package/dist/controls/SkiaCheckbox.js +0 -6
  17. package/dist/controls/SkiaDrawer.d.ts +2 -0
  18. package/dist/controls/SkiaDrawer.js +2 -0
  19. package/dist/controls/SkiaEditor.d.ts +12 -1
  20. package/dist/controls/SkiaEditor.js +46 -19
  21. package/dist/controls/SkiaGif.d.ts +3 -0
  22. package/dist/controls/SkiaGif.js +12 -2
  23. package/dist/controls/SkiaImage.js +15 -10
  24. package/dist/controls/SkiaLabel.d.ts +54 -3
  25. package/dist/controls/SkiaLabel.js +235 -52
  26. package/dist/controls/SkiaLabelFps.d.ts +16 -0
  27. package/dist/controls/SkiaLabelFps.js +41 -0
  28. package/dist/controls/SkiaLayout.d.ts +64 -7
  29. package/dist/controls/SkiaLayout.js +316 -19
  30. package/dist/controls/SkiaLottie.js +4 -2
  31. package/dist/controls/SkiaProgress.d.ts +2 -1
  32. package/dist/controls/SkiaProgress.js +6 -3
  33. package/dist/controls/SkiaRadioButton.js +4 -3
  34. package/dist/controls/SkiaScroll.d.ts +35 -5
  35. package/dist/controls/SkiaScroll.js +233 -23
  36. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  37. package/dist/controls/SkiaScrollBar.js +51 -2
  38. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  39. package/dist/controls/SkiaShaderCarousel.js +5 -0
  40. package/dist/controls/SkiaShape.d.ts +13 -2
  41. package/dist/controls/SkiaShape.js +50 -10
  42. package/dist/controls/SkiaSlider.d.ts +20 -2
  43. package/dist/controls/SkiaSlider.js +59 -5
  44. package/dist/controls/SkiaSprite.d.ts +1 -0
  45. package/dist/controls/SkiaSprite.js +4 -0
  46. package/dist/controls/SkiaSvg.js +1 -1
  47. package/dist/controls/SkiaSwitch.d.ts +0 -1
  48. package/dist/controls/SkiaSwitch.js +0 -6
  49. package/dist/controls/SkiaToggle.d.ts +25 -5
  50. package/dist/controls/SkiaToggle.js +58 -5
  51. package/dist/controls/SnappingLayout.d.ts +12 -1
  52. package/dist/controls/SnappingLayout.js +19 -1
  53. package/dist/core/Accessibility.d.ts +80 -0
  54. package/dist/core/Accessibility.js +365 -8
  55. package/dist/core/Animators.d.ts +37 -1
  56. package/dist/core/Animators.js +102 -9
  57. package/dist/core/Canvas.d.ts +108 -7
  58. package/dist/core/Canvas.js +363 -35
  59. package/dist/core/Easing.d.ts +12 -1
  60. package/dist/core/Easing.js +25 -1
  61. package/dist/core/Gestures.d.ts +48 -2
  62. package/dist/core/Gestures.js +95 -3
  63. package/dist/core/Hover.d.ts +47 -0
  64. package/dist/core/Hover.js +134 -0
  65. package/dist/core/KeyboardManager.d.ts +13 -0
  66. package/dist/core/KeyboardManager.js +18 -0
  67. package/dist/core/ScrollAnimators.js +9 -0
  68. package/dist/core/SkiaControl.d.ts +216 -7
  69. package/dist/core/SkiaControl.js +516 -59
  70. package/dist/core/SkiaEffect.d.ts +17 -0
  71. package/dist/core/SkiaEffect.js +43 -8
  72. package/dist/core/SkiaImageManager.js +1 -1
  73. package/dist/core/Styles.d.ts +51 -0
  74. package/dist/core/Styles.js +92 -0
  75. package/dist/core/Super.d.ts +17 -1
  76. package/dist/core/Super.js +35 -4
  77. package/dist/core/ViewsAdapter.d.ts +7 -2
  78. package/dist/core/ViewsAdapter.js +10 -3
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +4 -0
  81. package/dist/react/SkiaShell.js +33 -14
  82. package/dist/react/index.d.ts +15 -4
  83. package/dist/react/index.js +102 -15
  84. package/dist/react/reconciler.js +34 -12
  85. package/dist/vite/index.d.ts +9 -0
  86. package/dist/vite/index.js +36 -3
  87. package/package.json +33 -3
@@ -1,4 +1,5 @@
1
1
  import { Super } from "./Super";
2
+ import { SkiaStyles } from "./Styles";
2
3
  import { Colors, SKRect, ScaledSize, Thickness } from "./Types";
3
4
  import { RippleAnimator, SkiaValueAnimator } from "./Animators";
4
5
  import { Easing } from "./Easing";
@@ -21,12 +22,29 @@ export class CachedObject {
21
22
  this.Picture = Picture;
22
23
  this.Image = Image;
23
24
  }
24
- /** Replays the cache with its top-left moved to (left, top). */
25
- Draw(canvas, left, top) {
25
+ /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
26
+ Epoch = 0;
27
+ /** A picture recorded to be drawn distorted: the images in it sample smoothly (C# CachedObject.PlaybackDistorted). */
28
+ PlaybackDistorted = false;
29
+ /** A picture holding image blits, whose sampling was fixed when it was recorded (C# CachedObject.HasImageBlits). */
30
+ HasImageBlits = false;
31
+ /** C# PaintRenderObject: the blit paint is antialiased (else a turned bitmap keeps stair steps along its own border), dithered when distorted. */
32
+ static blitPaint;
33
+ static blitPaintDithered;
34
+ static BlitPaint(dither) {
35
+ const CK = Super.CK;
36
+ const make = () => { const p = new CK.Paint(); p.setAntiAlias(true); p.setDither(dither); return p; };
37
+ return dither ? (CachedObject.blitPaintDithered ??= make()) : (CachedObject.blitPaint ??= make());
38
+ }
39
+ /** Replays the cache with its top-left moved to (left, top); `smooth` = an image drawn distorted (linear + mipmaps). */
40
+ Draw(canvas, left, top, smooth = false) {
26
41
  const CK = Super.CK;
27
42
  if (this.Image) {
28
- // Nearest sampling: a cached bitmap is blitted 1:1, never resampled.
29
- canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Nearest, CK.MipmapMode.None, null);
43
+ // nearest keeps a bitmap blitted 1:1 sharp; once scaled, turned or projected it needs smooth sampling (C# 1e4610f6)
44
+ if (smooth)
45
+ canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Linear, CK.MipmapMode.Linear, CachedObject.BlitPaint(true));
46
+ else
47
+ canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Nearest, CK.MipmapMode.None, CachedObject.BlitPaint(false));
30
48
  }
31
49
  else if (this.Picture) {
32
50
  const saved = canvas.save();
@@ -35,9 +53,36 @@ export class CachedObject {
35
53
  canvas.restoreToCount(saved);
36
54
  }
37
55
  }
56
+ /**
57
+ * C# GetPictureImage: the picture of a picture cache (Operations, OperationsFull) drawn into an image, made on the
58
+ * first call (on `main`'s GPU context when given) and kept as long as this cache lives. Shader effects read it as
59
+ * their input, so on such a control they work on its own content, as on an Image cache.
60
+ */
61
+ GetPictureImage(main) {
62
+ if (this.pictureImage || !this.Picture)
63
+ return this.pictureImage;
64
+ const w = Math.ceil(this.Bounds.Width), h = Math.ceil(this.Bounds.Height);
65
+ if (w <= 0 || h <= 0)
66
+ return undefined;
67
+ const surface = main?.makeSurface({ ...main.imageInfo(), width: w, height: h }) ?? Super.CK.MakeSurface(w, h);
68
+ if (!surface)
69
+ return undefined;
70
+ const canvas = surface.getCanvas();
71
+ canvas.clear(Super.CK.TRANSPARENT);
72
+ canvas.translate(-this.Bounds.Left, -this.Bounds.Top);
73
+ canvas.drawPicture(this.Picture);
74
+ this.pictureImage = surface.makeImageSnapshot();
75
+ surface.delete();
76
+ return this.pictureImage;
77
+ }
78
+ pictureImage;
79
+ /** The picture drawn into an image, once GetPictureImage made it. */
80
+ get PictureImage() { return this.pictureImage; }
38
81
  Dispose() {
39
82
  this.Picture?.delete();
40
83
  this.Image?.delete();
84
+ this.pictureImage?.delete();
85
+ this.pictureImage = undefined;
41
86
  }
42
87
  }
43
88
  /**
@@ -86,6 +131,14 @@ export class SkiaControl {
86
131
  /** Degrees. */
87
132
  SkewX = 0;
88
133
  SkewY = 0;
134
+ /** Degrees, a 3D turn around the horizontal axis through the pivot, seen with perspective (C# RotationX). */
135
+ RotationX = 0;
136
+ /** Degrees, a 3D turn around the vertical axis through the pivot (C# RotationY). */
137
+ RotationY = 0;
138
+ /** Degrees, a 3D turn around the axis facing the viewer, after RotationX and RotationY (C# RotationZ). */
139
+ RotationZ = 0;
140
+ /** Depth: toward the viewer (+, bigger) or away (−, smaller), turned or flat; draws at 576 / (576 − TranslationZ) (C# TranslationZ). */
141
+ TranslationZ = 0;
89
142
  /** Pivot for Rotation/Scale/Skew as a fraction of the box (MAUI default 0.5). */
90
143
  AnchorX = 0.5;
91
144
  AnchorY = 0.5;
@@ -113,8 +166,9 @@ export class SkiaControl {
113
166
  get Scale() { return this.ScaleX; }
114
167
  set Scale(v) { this.ScaleX = v; this.ScaleY = v; }
115
168
  get HasTransform() {
116
- return this.TranslationX !== 0 || this.TranslationY !== 0 || this.Left !== 0 || this.Top !== 0 || this.Rotation !== 0 || this.ScaleX !== 1 || this.ScaleY !== 1 || this.SkewX !== 0 || this.SkewY !== 0;
169
+ return this.TranslationX !== 0 || this.TranslationY !== 0 || this.Left !== 0 || this.Top !== 0 || this.Rotation !== 0 || this.ScaleX !== 1 || this.ScaleY !== 1 || this.SkewX !== 0 || this.SkewY !== 0 || this.Has3d;
117
170
  }
171
+ get Has3d() { return this.RotationX !== 0 || this.RotationY !== 0 || this.RotationZ !== 0 || this.TranslationZ !== 0; }
118
172
  /** Matrix applied at the last render (canvas space), undefined when none; gestures map through its inverse. */
119
173
  RenderTransformMatrix;
120
174
  /** Generic numeric parameters (DrawnUi Value1/Value2): SkiaShape Arc uses start angle / sweep angle in degrees. */
@@ -131,6 +185,7 @@ export class SkiaControl {
131
185
  return;
132
186
  this.bindingContext = value;
133
187
  this.OnBindingContextChanged();
188
+ this.Superview?.Hover?.RequestCheck(); // a recycled cell under a still mouse now shows another item
134
189
  }
135
190
  /** Index of the bound item inside its ItemsSource, -1 when not templated. */
136
191
  ContextIndex = -1;
@@ -140,13 +195,18 @@ export class SkiaControl {
140
195
  /** What to cache for this subtree; see SkiaCacheType. Layouts default to None, SkiaLabel/SkiaSvg to Operations. */
141
196
  UseCache = "None";
142
197
  /** The cache type actually applied (None while caching is disabled globally). */
198
+ /**
199
+ * C# HostsBackdrop: a SkiaBackdrop inside asked this picture-cached control to draw live (a picture would replay
200
+ * the backdrop's screen copy frozen). Stays set after the backdrop is removed: the control just keeps drawing live.
201
+ */
202
+ HostsBackdrop = false;
143
203
  get UsingCacheType() {
144
204
  if (!Super.CacheEnabled)
145
205
  return "None";
146
206
  switch (this.UseCache) {
147
207
  case "None": return "None";
148
208
  case "Operations":
149
- case "OperationsFull": return "Operations";
209
+ case "OperationsFull": return this.HostsBackdrop ? "None" : "Operations";
150
210
  case "ImageDoubleBuffered": return "ImageDoubleBuffered";
151
211
  case "ImageComposite":
152
212
  case "ImageCompositeGPU": return "ImageComposite";
@@ -163,15 +223,52 @@ export class SkiaControl {
163
223
  compositeSurface;
164
224
  /** Own content or structure changed: the next composite record is a full one. */
165
225
  compositeFull = true;
226
+ /** Where the kept composite surface starts relative to DrawingRect (the effects margin it was laid out with). */
227
+ compositeInsetX = 0;
228
+ compositeInsetY = 0;
166
229
  /** Canvas-pixel bounds this control covered when its parent last recorded it (composite erase region). */
167
230
  lastCompositeBounds;
168
- /** What the last composite record did (diagnostics): "full" or "partial", the number of children re-recorded and which ones. */
169
- LastCompositeRecord = { Mode: "full", Children: 0, Dirty: [] };
231
+ /**
232
+ * What the last composite record did (diagnostics): "full" or "partial", the number of children re-recorded and which
233
+ * ones; Areas: the pixels erased and drawn again for changes deeper than a child (C# Areas), Changed: the controls
234
+ * those areas belong to (C# Changed).
235
+ */
236
+ LastCompositeRecord = { Mode: "full", Children: 0, Dirty: [], Areas: [], Changed: [] };
237
+ /** C# MaxCompositionAreas: more areas than this in one composite record draw it whole instead. */
238
+ static MaxCompositionAreas = 16;
239
+ /** C# MaxCompositionShare: areas covering more than this share of a composite draw it whole instead. */
240
+ static MaxCompositionShare = 0.5;
241
+ /**
242
+ * Changes deeper than a direct child since the last composite record (drawnui-cross 6m): the control, the direct child
243
+ * it is in, where it was drawn before the change (its own drawn bounds, as drawn) and its rect then.
244
+ */
245
+ compositeDeep;
246
+ /** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
247
+ static drawOrigin = null;
170
248
  /** The children a composite record can re-record individually; layouts return their views. */
171
249
  GetCompositeChildren() { return []; }
172
250
  /** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
173
251
  TrackChildAsDirty(child) { if (this.IsCacheComposite)
174
252
  this.DirtyChildrenInternal.add(child); }
253
+ static rejectRect = new Float32Array(4);
254
+ /** Its cache must be recorded before it can be drawn (none, dirty, another type, scale, size or GPU context). */
255
+ CacheStale(scale) {
256
+ const cacheType = this.UsingCacheType;
257
+ if (cacheType === "None")
258
+ return false;
259
+ const o = this.RenderObject, r = cacheType === "Operations" ? this.ExpandedCacheRect(scale) : this.AlignedCacheRect(scale);
260
+ return this.cacheDirty || !o || o.Type !== cacheType || o.Epoch !== (this.Superview?.GpuEpoch ?? 0) || o.Scale !== scale
261
+ || Math.round(o.Bounds.Width) !== Math.round(r.Width) || Math.round(o.Bounds.Height) !== Math.round(r.Height);
262
+ }
263
+ /** Its rect grown by its effects margin, through the matrix it is drawn with, lies outside the canvas clip. */
264
+ OutsideClip(canvas, scale) {
265
+ const b = SkiaControl.MapRect(this.RenderTransformMatrix, this.ExpandedCacheRect(scale)), q = SkiaControl.rejectRect;
266
+ q[0] = b.Left;
267
+ q[1] = b.Top;
268
+ q[2] = b.Right;
269
+ q[3] = b.Bottom;
270
+ return canvas.quickReject(q);
271
+ }
175
272
  /** Drawn bounds in canvas pixels including transforms and effects margins (C# GetTransformedDirtyBounds). */
176
273
  GetTransformedDirtyBounds() {
177
274
  const r = this.ExpandedCacheRect(this.RenderingScale), m = this.RenderTransformMatrix;
@@ -190,6 +287,57 @@ export class SkiaControl {
190
287
  /** Consume every gesture that lands on this control so nothing below (z-order) receives it. */
191
288
  BlockGesturesBelow = false;
192
289
  LockChildrenGestures = "Disabled";
290
+ // ---- hover (drawnui-cross 6m, C# ReceivesHover / IsHovered / HoverChanged) ----
291
+ receivesHover;
292
+ /**
293
+ * Whether this control takes mouse hover: the canvas sets IsHovered while the mouse is over it. Opt-in: false by
294
+ * default, true by default only in the controls that always had hover (SkiaButton, SkiaSlider, the toggles,
295
+ * SkiaRadioButton, SkiaCarousel, SkiaDrawer), never because a control has a tap handler. A HoverChanged handler turns
296
+ * it on (C# fluent OnHovered) unless it is set.
297
+ */
298
+ get ReceivesHover() { return this.receivesHover ?? (this.HoverChanged ? true : this.ReceivesHoverByDefault()); }
299
+ set ReceivesHover(value) { this.receivesHover = value; }
300
+ /** The value of ReceivesHover until it is set. */
301
+ ReceivesHoverByDefault() { return false; }
302
+ isHovered = false;
303
+ /**
304
+ * True while the mouse is over this control and it takes hover. Every such control under the pointer is hovered at
305
+ * once (a card and the button inside it, as CSS :hover); it stays as it is while content moves under the pointer and
306
+ * is checked again when that stops. Touch never hovers.
307
+ */
308
+ get IsHovered() { return this.isHovered; }
309
+ /** IsHovered changed: true when the mouse came over this control, false when it left (C# HoverChanged / OnHovered). */
310
+ HoverChanged;
311
+ /** Set by the canvas's HoverManager. */
312
+ ApplyHover(on) {
313
+ if (this.isHovered === on)
314
+ return;
315
+ this.isHovered = on;
316
+ this.HoverChanged?.(this, on);
317
+ }
318
+ /** A scroll, carousel or drawer moving its content right now: hover waits until it stops (C# IsScrolling / InTransition). */
319
+ MovesContent() { return false; }
320
+ /**
321
+ * Hover hit path (HoverManager): this control is under the point (in its own space), its children under it follow,
322
+ * top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
323
+ * controls under it get no hover (a popup over a list).
324
+ */
325
+ CollectHovered(point, into, xs, ys) {
326
+ into.push(this);
327
+ xs?.push(point.X);
328
+ ys?.push(point.Y);
329
+ if (!this.CheckChildrenGesturesLocked("Pointer")) {
330
+ const listeners = this.GetGestureListeners();
331
+ for (let i = listeners.length - 1; i >= 0; i--) {
332
+ const child = listeners[i];
333
+ if (!child.IsVisible || child.InputTransparent || !this.IsGestureForChild(child, point))
334
+ continue;
335
+ if (child.CollectHovered(child.TransformPointToLocalSpace(point), into, xs, ys))
336
+ return true;
337
+ }
338
+ }
339
+ return this.BlockGesturesBelow;
340
+ }
193
341
  // ---- touch feedback (same names as DrawnUi) ----
194
342
  TouchEffectColor = Colors.White;
195
343
  /** Plays a ripple (from the tap point) when Tapped fires. */
@@ -198,6 +346,11 @@ export class SkiaControl {
198
346
  AnimationTappedSpeed = 0;
199
347
  /** Overlay effects drawn above this control's content every frame (ripple etc). */
200
348
  PostAnimators = [];
349
+ /**
350
+ * C# PendingUnattachedAnimators: animators started while no Canvas was above this control yet (e.g. a game loop
351
+ * started in a constructor). They start on the control's first arrange, where C# starts them in SuperViewChanged.
352
+ */
353
+ PendingUnattachedAnimators;
201
354
  // ---- visual effects (C# VisualEffects: attached SkiaEffect objects) ----
202
355
  visualEffects = [];
203
356
  /** Post-renderer effects among VisualEffects (C# EffectPostRenderers). */
@@ -229,10 +382,27 @@ export class SkiaControl {
229
382
  return [];
230
383
  return this.EffectPostRenderers.filter((e) => e.NeedApply);
231
384
  }
232
- /** C# CachedImage: the cached texture and the canvas rect it was rasterized over (Image caches only). */
385
+ /**
386
+ * C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
387
+ * re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
388
+ * at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
389
+ * there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
390
+ */
233
391
  get CachedImage() {
234
392
  const c = this.RenderObject;
235
- return c?.Image ? { Image: c.Image, Bounds: c.Bounds } : undefined;
393
+ if (!c)
394
+ return undefined;
395
+ if (c.Image) {
396
+ const r = this.AlignedCacheRect(c.Scale);
397
+ return { Image: c.Image, Bounds: SKRect.Create(r.Left, r.Top, c.Bounds.Width, c.Bounds.Height) };
398
+ }
399
+ // a picture cache: its picture drawn into an image (made in RenderContent), so effects work on this control's
400
+ // content instead of a snapshot of what is behind it (C# caa1c8c1)
401
+ const image = c.PictureImage;
402
+ if (!image)
403
+ return undefined;
404
+ const r = this.ExpandedCacheRect(c.Scale);
405
+ return { Image: image, Bounds: SKRect.Create(r.Left, r.Top, image.width(), image.height()) };
236
406
  }
237
407
  /** Clip overlay effects to the control's shape (CreateClip). */
238
408
  ClipEffects = true;
@@ -250,6 +420,22 @@ export class SkiaControl {
250
420
  // ---- tree ----
251
421
  Parent;
252
422
  /** Containers override; a leaf control cannot host children. */
423
+ styledInitially = false;
424
+ /**
425
+ * C# ApplyInitialStyles: takes the property defaults registered with `ConfigureStyles` for this control's class.
426
+ * The reconciler calls it with `atConstruction` right after `new`, before the JSX props, so props win; a control
427
+ * built in code-behind is styled on its first measure and keeps whatever the code already set.
428
+ */
429
+ ApplyInitialStyles(atConstruction = false) {
430
+ this.styledInitially = true;
431
+ if (!SkiaStyles.IsEmpty)
432
+ SkiaStyles.Apply(this, atConstruction);
433
+ }
434
+ /**
435
+ * C# IsChildrenItem: added by the app (JSX children or the `Children` list), not a part the control made itself. A new
436
+ * `Children` list drops only these, so a control's own parts keep their place.
437
+ */
438
+ IsChildrenItem = false;
253
439
  AddSubView(_control) { throw new Error(`DrawnUi: ${this.constructor.name} cannot host children`); }
254
440
  InsertSubView(_index, control) { this.AddSubView(control); }
255
441
  RemoveSubView(_control) { }
@@ -271,6 +457,8 @@ export class SkiaControl {
271
457
  * this is what keeps a cached tree from re-measuring text every frame.
272
458
  */
273
459
  Measure(widthConstraint, heightConstraint, scale) {
460
+ if (!this.styledInitially)
461
+ this.ApplyInitialStyles(false);
274
462
  if (!this.NeedMeasure && this.RenderingScale === scale
275
463
  && Object.is(this.lastWidthConstraint, widthConstraint) && Object.is(this.lastHeightConstraint, heightConstraint)) {
276
464
  return this.MeasuredSize;
@@ -347,12 +535,37 @@ export class SkiaControl {
347
535
  const fullW = destination.Width - m.HorizontalThickness * scale, fullH = destination.Height - m.VerticalThickness * scale;
348
536
  const x = availL + SkiaControl.Align(this.HorizontalOptions, fullW, w);
349
537
  const y = availT + SkiaControl.Align(this.VerticalOptions, fullH, h);
538
+ const old = this.DrawingRect;
350
539
  this.DrawingRect = SKRect.Create(x, y, w, h);
540
+ this.FollowMatrix(old);
351
541
  this.OnLayoutChanged();
542
+ if (this.PendingUnattachedAnimators)
543
+ this.StartPendingAnimators();
352
544
  // DrawnUi OnLayoutReady: an accessible control registers itself on its first layout
353
545
  if (!this.registeredWithAccessibility && this.IsAccessibilityElement)
354
546
  this.NotifyAccessibility();
355
547
  }
548
+ /**
549
+ * A cached ancestor replays its picture where a re-arrange moved it (a scroll) without rendering this control again,
550
+ * so the matrix made at its last Render moves with its rect: taps, hover and the accessibility rect stay on what is
551
+ * drawn (C# offsets the cached render tree instead). A control that renders again gets a fresh matrix there.
552
+ */
553
+ FollowMatrix(old) {
554
+ const m = this.RenderTransformMatrix, r = this.DrawingRect;
555
+ if (!m || (old.Left === r.Left && old.Top === r.Top))
556
+ return;
557
+ const M = Super.CK.Matrix, dx = r.Left - old.Left, dy = r.Top - old.Top;
558
+ this.RenderTransformMatrix = M.multiply(M.translated(dx, dy), m, M.translated(-dx, -dy));
559
+ }
560
+ /** C# SuperViewChanged: starts the animators queued before this control was attached. */
561
+ StartPendingAnimators() {
562
+ if (!this.Superview)
563
+ return;
564
+ const pending = this.PendingUnattachedAnimators;
565
+ this.PendingUnattachedAnimators = undefined;
566
+ for (const animator of pending)
567
+ animator.Start();
568
+ }
356
569
  // ---- accessibility (DrawnUi ISkiaAccessibilityNode) ----
357
570
  static nextAccessibilityId = 1;
358
571
  /** Stable identity for the DOM overlay. */
@@ -387,8 +600,12 @@ export class SkiaControl {
387
600
  this.accessibilityHint = v;
388
601
  this.AccessibilityChanged();
389
602
  } }
390
- /** Tab stop + activation; unset = true when the control handles Tapped. */
391
- get AccessibilityCanInteract() { return this.accessibilityCanInteract ?? this.DefaultAccessibilityCanInteract(); }
603
+ /**
604
+ * Whether keyboard navigation and assistive technology may focus and use this node: the explicit value, else the class
605
+ * default (true when the control handles Tapped), and in both cases only while a tap could reach the control
606
+ * (CanReceiveGesture), so Tab, Enter / Space and screen-reader activation never use a control the pointer cannot.
607
+ */
608
+ get AccessibilityCanInteract() { return (this.accessibilityCanInteract ?? this.DefaultAccessibilityCanInteract()) && this.CanReceiveGesture("Tapped"); }
392
609
  set AccessibilityCanInteract(v) { if (this.accessibilityCanInteract !== v) {
393
610
  this.accessibilityCanInteract = v;
394
611
  this.AccessibilityChanged();
@@ -399,6 +616,8 @@ export class SkiaControl {
399
616
  * host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
400
617
  */
401
618
  WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
619
+ /** A range control's value for screen readers (slider, progress); undefined for everything else (drawnui-cross 6c). */
620
+ get AccessibilityValue() { return undefined; }
402
621
  /** aria-pressed for toggles; undefined = not a toggle. */
403
622
  get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
404
623
  set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
@@ -473,6 +692,39 @@ export class SkiaControl {
473
692
  }
474
693
  /** Overlay focus arrives on / leaves this node (inputs may activate their sink here). */
475
694
  OnAccessibilityFocused(_focused) { }
695
+ /**
696
+ * Keyboard navigation hands this control the keys it may use while it holds keyboard focus (ArrowLeft / Right / Up /
697
+ * Down, Home, End, PageUp, PageDown, DOM `event.code` names). Return true when the key was used; the default uses none,
698
+ * so the key moves focus inside the arrow-key group around the control.
699
+ */
700
+ OnAccessibilityKey(_key) { return false; }
701
+ /**
702
+ * Whether a pointer gesture of this kind would reach this control, by the rules the gesture dispatch applies: the
703
+ * control and every ancestor draw, none of them is InputTransparent, no ancestor keeps this gesture from its children
704
+ * (LockChildrenGestures, PassNone included) and the control accepts input (AcceptsInput). Opacity does not count, as
705
+ * for the pointer. Keyboard navigation uses it: Tapped for Tab and Enter / Space, Panning for the arrow keys.
706
+ */
707
+ CanReceiveGesture(gesture) {
708
+ if (!this.IsVisible || this.IsDisposed || this.InputTransparent || !this.AcceptsInput())
709
+ return false;
710
+ for (let p = this.Parent; p; p = p.Parent)
711
+ if (!p.IsVisible || p.IsDisposed || p.InputTransparent || p.CheckChildrenGesturesLocked(gesture))
712
+ return false;
713
+ return true;
714
+ }
715
+ /**
716
+ * False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
717
+ * Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
718
+ */
719
+ AcceptsInput() { return true; }
720
+ /**
721
+ * What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
722
+ * press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
723
+ * it (a scroll behind it still scrolls).
724
+ */
725
+ BlockedGestureResult(args) {
726
+ return args.Type === "Down" || args.Type === "Tapped" || args.Type === "LongPressing" ? this : null;
727
+ }
476
728
  NotifyAccessibilityFocused(focused) { this.Superview?.AccessibilityManager.NotifyFocused(focused ? this : undefined); }
477
729
  /** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
478
730
  static SmartMax(a, b) {
@@ -538,8 +790,25 @@ export class SkiaControl {
538
790
  const applyOpacity = this.Opacity < 1;
539
791
  // C# Left/Top: a cached control is blitted at an offset, no matrix and no save/restore around the subtree
540
792
  const offsetOnly = (this.Left !== 0 || this.Top !== 0) && this.UsingCacheType !== "None" && this.TranslationX === 0 && this.TranslationY === 0
541
- && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0;
793
+ && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !this.Has3d;
542
794
  const needTransform = this.HasTransform && !offsetOnly;
795
+ let dx = 0, dy = 0;
796
+ if (needTransform) {
797
+ this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
798
+ }
799
+ else if (offsetOnly) {
800
+ dx = this.Left * ctx.Scale;
801
+ dy = this.Top * ctx.Scale;
802
+ this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
803
+ }
804
+ else {
805
+ this.RenderTransformMatrix = undefined;
806
+ }
807
+ // drawn entirely outside the clip: skipped with its subtree (C# layout Virtualisation, DrawnUi.Rust); the matrix
808
+ // above stays current for hit tests. A cache that must be recorded is still recorded here, as before, so its cost
809
+ // does not move into the frame where the control scrolls in (DrawnUi.Rust records it then)
810
+ if (this.OutsideClip(canvas, ctx.Scale) && !this.CacheStale(ctx.Scale))
811
+ return;
543
812
  let saved = false;
544
813
  // same as DrawnUi: opacity = a layer with alpha, transforms = canvas matrix around the whole subtree (cache included)
545
814
  if (applyOpacity) {
@@ -553,19 +822,8 @@ export class SkiaControl {
553
822
  canvas.save();
554
823
  saved = true;
555
824
  }
556
- let dx = 0, dy = 0;
557
- if (needTransform) {
558
- this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
825
+ if (needTransform)
559
826
  canvas.concat(this.RenderTransformMatrix);
560
- }
561
- else if (offsetOnly) {
562
- dx = this.Left * ctx.Scale;
563
- dy = this.Top * ctx.Scale;
564
- this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
565
- }
566
- else {
567
- this.RenderTransformMatrix = undefined;
568
- }
569
827
  if (this.IsClippedToBounds) {
570
828
  if (!saved) {
571
829
  canvas.save();
@@ -578,11 +836,12 @@ export class SkiaControl {
578
836
  if (saved)
579
837
  canvas.restore();
580
838
  }
581
- /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · T(pivot) · translation, in canvas pixels. */
839
+ /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · 3D · T(pivot) · translation, in canvas pixels. */
582
840
  CreateRenderTransformMatrix(destination, scale) {
583
841
  const M = Super.CK.Matrix;
584
842
  const moveX = (this.TranslationX + this.Left) * scale, moveY = (this.TranslationY + this.Top) * scale;
585
- if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0)
843
+ const has3d = this.Has3d;
844
+ if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !has3d)
586
845
  return M.translated(moveX, moveY);
587
846
  const px = destination.Left + destination.Width * this.AnchorX;
588
847
  const py = destination.Top + destination.Height * this.AnchorY;
@@ -591,7 +850,36 @@ export class SkiaControl {
591
850
  const scaleSkew = [this.ScaleX, kx, 0, ky, this.ScaleY, 0, 0, 0, 1];
592
851
  const rotation = this.Rotation !== 0 ? M.rotated((Math.PI * this.Rotation) / 180) : M.identity();
593
852
  // multiply(a, b, c) = a·b·c, the right-most applies first
594
- return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
853
+ if (!has3d)
854
+ return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
855
+ return M.multiply(M.translated(px + moveX, py + moveY), SkiaControl.Create3dMatrix(this.RotationX, this.RotationY, this.RotationZ, this.TranslationZ), scaleSkew, rotation, M.translated(-px, -py));
856
+ }
857
+ /** C# camera distance of the 3D transforms (Sk3dView.CameraDistance): 8 inches at 72 dpi, in canvas pixels. */
858
+ static CameraDistance = 576;
859
+ /**
860
+ * C# Create3dMatrix (Sk3dView): RotationX, then RotationY, then RotationZ (negated) around the pivot, projected from
861
+ * the camera; TranslationZ is depth, a flat control at depth z draws at camera / (camera − z).
862
+ */
863
+ static Create3dMatrix(rotationX, rotationY, rotationZ, translationZ) {
864
+ const d = Math.PI / 180, camera = SkiaControl.CameraDistance;
865
+ const sx = Math.sin(rotationX * d), cx = Math.cos(rotationX * d), sy = Math.sin(rotationY * d), cy = Math.cos(rotationY * d);
866
+ const sz = Math.sin(-rotationZ * d), cz = Math.cos(-rotationZ * d);
867
+ return [
868
+ cy * cz, cy * sz, 0,
869
+ sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, 0,
870
+ (cx * sy * cz + sx * sz) / camera, (cx * sy * sz - sx * cz) / camera, Math.max(camera - translationZ, 1) / camera,
871
+ ];
872
+ }
873
+ /** C# SkiaSamplingOptions.IsMoveOnly: the matrix only moves (or mirrors), so a bitmap keeps its pixels 1:1. */
874
+ static IsMoveOnly(m) {
875
+ return m[1] === 0 && m[3] === 0 && m[6] === 0 && m[7] === 0 && m[8] === 1 && Math.abs(m[0]) === 1 && Math.abs(m[4]) === 1;
876
+ }
877
+ /**
878
+ * C# WillDrawDistorted: what this control draws here ends up scaled, turned, skewed or projected, by its own transforms
879
+ * or a live ancestor's (both in the canvas matrix) or by the picture it is recorded into. Images then sample smoothly.
880
+ */
881
+ WillDrawDistorted(ctx) {
882
+ return !!ctx.Context.PlaybackDistorted || !SkiaControl.IsMoveOnly(ctx.Context.Canvas.getTotalMatrix());
595
883
  }
596
884
  /** Maps a point given in the parent's space into this control's untransformed space (DrawnUi TransformPointToLocalSpace). */
597
885
  TransformPointToLocalSpace(point) {
@@ -627,26 +915,39 @@ export class SkiaControl {
627
915
  }
628
916
  else {
629
917
  const r = cacheType === "Operations" ? this.ExpandedCacheRect(ctx.Scale) : this.AlignedCacheRect(ctx.Scale);
630
- const stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType
631
- || this.RenderObject.Scale !== ctx.Scale
632
- || Math.round(this.RenderObject.Bounds.Width) !== Math.round(r.Width) || Math.round(this.RenderObject.Bounds.Height) !== Math.round(r.Height);
633
- if (stale)
918
+ const epoch = this.Superview?.GpuEpoch ?? 0;
919
+ if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
920
+ this.DisposePrevious(); // made on a lost GPU context
921
+ // a picture replays its image blits with the sampling chosen when it was recorded: recorded again once its
922
+ // playback turns distorted or straight (C# SamplingMismatch), else a cached child of a turning card stays jagged
923
+ const o = this.RenderObject;
924
+ if (this.CacheStale(ctx.Scale) || (o?.Picture && o.HasImageBlits && o.PlaybackDistorted !== this.WillDrawDistorted(ctx)))
634
925
  this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
635
926
  if (this.RenderObject) {
636
- // C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
637
- // and paint the result; a picture cache has no texture, so it is replayed first and snapshotted by the effect
638
- if (post.length === 0 || !this.RenderObject.Image)
639
- this.RenderObject.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy);
927
+ // C# DrawRenderObject: with post renderers a cache is not blitted, the effects sample it (CachedImage) and paint
928
+ // the result, so the shader replaces the control; a picture cache hands them its picture drawn into an image
929
+ // (made once per cache, C# GetPictureImage); drawn as before only when that image could not be made
930
+ const cache = this.RenderObject;
931
+ if (post.length > 0 && cache.Picture)
932
+ cache.GetPictureImage(ctx.Context.Surface);
933
+ if (post.length === 0 || !(cache.Image ?? cache.PictureImage))
934
+ this.DrawCache(cache, ctx, r.Left + dx, r.Top + dy);
640
935
  for (const e of post)
641
936
  e.Render(own);
642
937
  }
643
938
  else if (this.RenderObjectPrevious)
644
- this.RenderObjectPrevious.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy); // double buffer: last good frame
939
+ this.DrawCache(this.RenderObjectPrevious, ctx, r.Left + dx, r.Top + dy); // double buffer: last good frame
645
940
  else
646
941
  this.DrawPlaceholder(own);
647
942
  }
648
943
  this.ExecutePostAnimators(own);
649
944
  }
945
+ /** Blits a cache: an image samples smoothly when drawn distorted; a picture being recorded learns it holds image blits. */
946
+ DrawCache(cache, ctx, left, top) {
947
+ if (cache.Image || cache.HasImageBlits)
948
+ ctx.Context.HasImageBlits = true;
949
+ cache.Draw(ctx.Context.Canvas, left, top, !!cache.Image && this.WillDrawDistorted(ctx));
950
+ }
650
951
  /** Background + Paint(): the part of the control that a cache captures. */
651
952
  PaintContent(ctx) {
652
953
  if (this.BackgroundColor || (this.FillGradient && this.FillGradientPaintsBackground()) || this.PaintsBackgroundWithoutColor())
@@ -681,11 +982,16 @@ export class SkiaControl {
681
982
  }
682
983
  if (cacheType === "Operations") {
683
984
  const recorder = new CK.PictureRecorder();
985
+ const playbackDistorted = this.WillDrawDistorted(ctx);
684
986
  const canvas = recorder.beginRecording(CK.LTRBRect(r.Left, r.Top, r.Right, r.Bottom));
685
- this.PaintContent({ ...ctx, Context: { ...ctx.Context, Canvas: canvas, Recording: true } });
987
+ const recording = { ...ctx.Context, Canvas: canvas, Recording: true, PlaybackDistorted: playbackDistorted, HasImageBlits: false };
988
+ this.PaintContent({ ...ctx, Context: recording });
686
989
  const picture = recorder.finishRecordingAsPicture();
687
990
  recorder.delete();
688
991
  this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
992
+ this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
993
+ this.RenderObject.PlaybackDistorted = playbackDistorted;
994
+ this.RenderObject.HasImageBlits = recording.HasImageBlits;
689
995
  }
690
996
  else if (cacheType === "ImageComposite") {
691
997
  this.CreateCompositeRenderingObject(ctx, r, w, h);
@@ -709,6 +1015,7 @@ export class SkiaControl {
709
1015
  const image = offscreen.makeImageSnapshot();
710
1016
  offscreen.delete();
711
1017
  this.RenderObject = new CachedObject(cacheType, r, ctx.Scale, undefined, image);
1018
+ this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
712
1019
  }
713
1020
  this.cacheDirty = false;
714
1021
  }
@@ -727,7 +1034,9 @@ export class SkiaControl {
727
1034
  }
728
1035
  let surface = this.compositeSurface;
729
1036
  const prev = this.RenderObject;
730
- const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale
1037
+ const epoch = this.Superview?.GpuEpoch ?? 0;
1038
+ // the kept surface belongs to the GPU context it was made on: after a context restore it is made again
1039
+ const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale && prev.Epoch === epoch
731
1040
  && Math.round(prev.Bounds.Width) === w && Math.round(prev.Bounds.Height) === h;
732
1041
  if (!sameGeometry) {
733
1042
  surface?.delete();
@@ -740,14 +1049,46 @@ export class SkiaControl {
740
1049
  }
741
1050
  const canvas = surface.getCanvas();
742
1051
  const children = this.GetCompositeChildren();
743
- const partial = sameGeometry && !this.compositeFull && this.DirtyChildrenInternal.size > 0 && children.length > 0;
1052
+ const deep = this.compositeDeep;
1053
+ // the same size with another effects margin moves the content inside the surface: record whole (C# 97683f19)
1054
+ const insetX = r.Left - this.DrawingRect.Left, insetY = r.Top - this.DrawingRect.Top;
1055
+ const sameInset = insetX === this.compositeInsetX && insetY === this.compositeInsetY;
1056
+ this.compositeInsetX = insetX;
1057
+ this.compositeInsetY = insetY;
1058
+ let partial = sameGeometry && sameInset && !this.compositeFull && children.length > 0 && (this.DirtyChildrenInternal.size > 0 || !!deep?.length);
1059
+ // changes deeper than a child: only their areas, the child they are in drawn clipped to them (drawnui-cross 6m)
1060
+ const areas = [], changed = [], inside = new Set();
1061
+ if (partial && deep?.length) {
1062
+ let covered = 0;
1063
+ for (const d of deep) {
1064
+ if (!children.includes(d.Child) || this.DirtyChildrenInternal.has(d.Child))
1065
+ continue;
1066
+ const area = SkiaControl.DeepArea(d, this, ctx.Scale);
1067
+ if (area === null) {
1068
+ partial = false;
1069
+ break;
1070
+ }
1071
+ if (!area) {
1072
+ this.DirtyChildrenInternal.add(d.Child);
1073
+ continue;
1074
+ }
1075
+ areas.push(area);
1076
+ changed.push(d.Origin);
1077
+ inside.add(d.Child);
1078
+ covered += area.Width * area.Height;
1079
+ }
1080
+ if (areas.length > SkiaControl.MaxCompositionAreas || covered > r.Width * r.Height * SkiaControl.MaxCompositionShare)
1081
+ partial = false;
1082
+ }
1083
+ if (deep)
1084
+ deep.length = 0;
744
1085
  const offset = prev && sameGeometry ? { X: r.Left - prev.Bounds.Left, Y: r.Top - prev.Bounds.Top } : { X: 0, Y: 0 };
745
1086
  const saved = canvas.save();
746
1087
  canvas.translate(-r.Left, -r.Top);
747
1088
  if (partial) {
748
1089
  // dirty = reported children + siblings intersecting their old or new bounds (C# makes intersecting children dirty too)
749
1090
  const dirty = new Set();
750
- const rects = [];
1091
+ const rects = [...areas];
751
1092
  const boundsOf = (c) => {
752
1093
  const out = [c.GetTransformedDirtyBounds()];
753
1094
  if (c.lastCompositeBounds)
@@ -763,7 +1104,7 @@ export class SkiaControl {
763
1104
  while (grew) {
764
1105
  grew = false;
765
1106
  for (const c of children) {
766
- if (dirty.has(c) || !c.IsVisible)
1107
+ if (dirty.has(c) || inside.has(c) || !c.IsVisible)
767
1108
  continue;
768
1109
  const own = boundsOf(c);
769
1110
  if (rects.some((d) => own.some((o) => o.Left < d.Right && d.Left < o.Right && o.Top < d.Bottom && d.Top < o.Bottom))) {
@@ -790,14 +1131,18 @@ export class SkiaControl {
790
1131
  this.DirtyChildrenInternal.clear();
791
1132
  for (const c of dirty)
792
1133
  this.DirtyChildrenInternal.add(c);
1134
+ for (const c of inside)
1135
+ this.DirtyChildrenInternal.add(c);
1136
+ // children entirely outside the redrawn areas are skipped by their own clip check (Render)
793
1137
  this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
794
1138
  this.IsRenderingWithComposition = false;
795
- this.LastCompositeRecord = { Mode: "partial", Children: dirty.size, Dirty: [...dirty] };
1139
+ const redrawn = children.filter((c) => this.DirtyChildrenInternal.has(c));
1140
+ this.LastCompositeRecord = { Mode: "partial", Children: redrawn.length, Dirty: redrawn, Areas: areas, Changed: changed };
796
1141
  }
797
1142
  else {
798
1143
  canvas.clear(CK.TRANSPARENT);
799
1144
  this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
800
- this.LastCompositeRecord = { Mode: "full", Children: children.length, Dirty: children };
1145
+ this.LastCompositeRecord = { Mode: "full", Children: children.length, Dirty: children, Areas: [], Changed: [] };
801
1146
  }
802
1147
  canvas.restoreToCount(saved);
803
1148
  for (const c of children)
@@ -807,6 +1152,7 @@ export class SkiaControl {
807
1152
  const image = surface.makeImageSnapshot();
808
1153
  const old = this.RenderObject;
809
1154
  this.RenderObject = new CachedObject("ImageComposite", r, ctx.Scale, undefined, image);
1155
+ this.RenderObject.Epoch = epoch;
810
1156
  if (old) {
811
1157
  const sv = this.Superview;
812
1158
  if (sv)
@@ -842,7 +1188,11 @@ export class SkiaControl {
842
1188
  /** Marks the cache stale; re-recorded on the next frame (a composite records fully: its own content changed). */
843
1189
  InvalidateCache() { this.cacheDirty = true; this.compositeFull = true; }
844
1190
  /** Effects margin recomputed on the next use (C# InvalidateEffectsMargin). */
845
- InvalidateEffectsMargin() { this.effectsMarginCache = undefined; }
1191
+ InvalidateEffectsMargin() {
1192
+ // a layout's margin aggregates what its children paint outside it: every ancestor recomputes too (C# 97683f19)
1193
+ for (let c = this; c; c = c.Parent)
1194
+ c.effectsMarginCache = undefined;
1195
+ }
846
1196
  /** Draws PostAnimators above content; an effect returning true asks for another frame. */
847
1197
  ExecutePostAnimators(ctx) {
848
1198
  if (this.PostAnimators.length === 0)
@@ -1024,6 +1374,7 @@ export class SkiaControl {
1024
1374
  this.ownAnimations.clear();
1025
1375
  for (const e of [...this.PostAnimators])
1026
1376
  e.Stop?.();
1377
+ this.PendingUnattachedAnimators = undefined;
1027
1378
  this.PostAnimators.length = 0;
1028
1379
  this.DestroyRenderingObject();
1029
1380
  this.compositeSurface?.delete();
@@ -1052,32 +1403,134 @@ export class SkiaControl {
1052
1403
  }
1053
1404
  /** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
1054
1405
  Repaint() {
1055
- this.Superview?.Update();
1406
+ if (!this.UnderHiddenParent())
1407
+ this.Superview?.Update();
1056
1408
  }
1409
+ /**
1410
+ * A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
1411
+ * parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
1412
+ * change still draws: it is the one that hides or shows it.
1413
+ */
1414
+ UnderHiddenParent() {
1415
+ for (let p = this.Parent; p; p = p.Parent)
1416
+ if (!p.IsVisible)
1417
+ return true;
1418
+ return false;
1419
+ }
1420
+ /** The control or a parent is hidden: its animators pause until it shows again. */
1421
+ HiddenInTree() { return !this.IsVisible || this.UnderHiddenParent(); }
1422
+ /** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
1423
+ static invalidateOrigin = null;
1057
1424
  /**
1058
1425
  * Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
1059
1426
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
1060
1427
  */
1061
1428
  RepaintComposition() {
1062
- let child = this, p = this.Parent;
1063
- while (p) {
1429
+ this.ReportToComposites();
1430
+ for (let p = this.Parent; p; p = p.Parent)
1064
1431
  p.cacheDirty = true;
1065
- p.TrackChildAsDirty(child);
1066
- child = p;
1067
- p = p.Parent;
1068
- }
1069
1432
  this.Repaint();
1070
1433
  }
1434
+ /**
1435
+ * Update() for a change of how this control looks, not of its size (a color set through props, C# NeedDraw): the same
1436
+ * re-measure and cache invalidation, but an ImageComposite above redraws only this control's area instead of
1437
+ * recording whole (drawnui-cross 6m). The record still goes whole when the control's rect changed after all.
1438
+ */
1439
+ UpdateDraw() {
1440
+ this.ReportToComposites();
1441
+ const outer = SkiaControl.drawOrigin;
1442
+ SkiaControl.drawOrigin = this;
1443
+ try {
1444
+ this.InvalidateMeasure();
1445
+ }
1446
+ finally {
1447
+ SkiaControl.drawOrigin = outer;
1448
+ }
1449
+ }
1450
+ /**
1451
+ * A change of this control that keeps its layout, to every ImageComposite above: a direct child is drawn again whole
1452
+ * (C# TrackChildAsDirty), a deeper control by its area, read now as it was drawn (its last matrix and effects margin).
1453
+ */
1454
+ ReportToComposites() {
1455
+ let before;
1456
+ let child = this;
1457
+ for (let p = this.Parent; p; child = p, p = p.Parent) {
1458
+ if (!p.IsCacheComposite)
1459
+ continue;
1460
+ if (child === this) {
1461
+ p.TrackChildAsDirty(child);
1462
+ continue;
1463
+ }
1464
+ const deep = p.compositeDeep ??= [];
1465
+ if (deep.some((d) => d.Origin === this))
1466
+ continue;
1467
+ before ??= this.GetTransformedDirtyBounds();
1468
+ const r = this.DrawingRect;
1469
+ deep.push({ Origin: this, Child: child, Before: before, Rect: new SKRect(r.Left, r.Top, r.Right, r.Bottom) });
1470
+ }
1471
+ }
1472
+ /** The matrix this control will be drawn with (Render), from its current properties; undefined when none. */
1473
+ CurrentMatrix(scale) {
1474
+ return this.HasTransform ? this.CreateRenderTransformMatrix(this.DrawingRect, scale) : undefined;
1475
+ }
1476
+ /** Where this control will be drawn: its rect grown by its effects margin, through its current matrix. */
1477
+ DrawnBoundsNow(scale) { return SkiaControl.MapRect(this.CurrentMatrix(scale), this.ExpandedCacheRect(scale)); }
1478
+ static MapRect(m, r) {
1479
+ if (!m)
1480
+ return r;
1481
+ const pts = Super.CK.Matrix.mapPoints(m, [r.Left, r.Top, r.Right, r.Top, r.Right, r.Bottom, r.Left, r.Bottom]);
1482
+ return new SKRect(Math.min(pts[0], pts[2], pts[4], pts[6]), Math.min(pts[1], pts[3], pts[5], pts[7]), Math.max(pts[0], pts[2], pts[4], pts[6]), Math.max(pts[1], pts[3], pts[5], pts[7]));
1483
+ }
1484
+ /** Its output depends on all of its content (a visual effect; a backdrop samples below): a change inside redraws it whole. */
1485
+ get SamplesContent() { return !this.DisableEffects && this.visualEffects.length > 0; }
1486
+ /**
1487
+ * Where a deep change shows inside the composite (drawnui-cross 6m rule b): the control as drawn before and as drawn
1488
+ * now, then through the transform of every ancestor up to the composite. undefined: draw the child whole (an effect
1489
+ * or a backdrop on the way, the control gone from the child); null: the layout changed, record whole.
1490
+ */
1491
+ static DeepArea(d, composite, scale) {
1492
+ const o = d.Origin, r = o.DrawingRect;
1493
+ if (r.Left !== d.Rect.Left || r.Top !== d.Rect.Top || r.Right !== d.Rect.Right || r.Bottom !== d.Rect.Bottom)
1494
+ return null;
1495
+ if (!o.IsVisible)
1496
+ return undefined;
1497
+ const now = o.DrawnBoundsNow(scale);
1498
+ let area = new SKRect(Math.min(now.Left, d.Before.Left), Math.min(now.Top, d.Before.Top), Math.max(now.Right, d.Before.Right), Math.max(now.Bottom, d.Before.Bottom));
1499
+ let p = o.Parent, child = o;
1500
+ for (; p && p !== composite; child = p, p = p.Parent) {
1501
+ if (p.SamplesContent || !p.IsVisible)
1502
+ return undefined;
1503
+ area = SkiaControl.MapRect(p.CurrentMatrix(scale), area);
1504
+ }
1505
+ return p === composite && child === d.Child ? area : undefined;
1506
+ }
1507
+ /**
1508
+ * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
1509
+ * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when
1510
+ * NeedMeasure is set. Used by SkiaScroll for its scroll bars.
1511
+ */
1512
+ IsParentIndependent = false;
1071
1513
  /** Size or content may have changed: this control and all ancestors remeasure and re-record. */
1072
1514
  InvalidateMeasure() {
1073
1515
  this.NeedMeasure = true;
1074
1516
  this.cacheDirty = true;
1075
- this.compositeFull = true; // structure may change: a composite cannot patch it
1517
+ // structure may change: a composite cannot patch it; a draw-only change (UpdateDraw) reported its area instead
1518
+ if (SkiaControl.drawOrigin === null || SkiaControl.drawOrigin === this)
1519
+ this.compositeFull = true;
1076
1520
  this.effectsMarginCache = undefined;
1077
- if (this.Parent)
1078
- this.Parent.InvalidateMeasure();
1079
- else
1080
- this.Superview?.Update();
1521
+ const outer = SkiaControl.invalidateOrigin;
1522
+ SkiaControl.invalidateOrigin ??= this;
1523
+ try {
1524
+ if (this.Parent && this.IsParentIndependent)
1525
+ this.RepaintComposition();
1526
+ else if (this.Parent)
1527
+ this.Parent.InvalidateMeasure();
1528
+ else if (!SkiaControl.invalidateOrigin.UnderHiddenParent())
1529
+ this.Superview?.Update();
1530
+ }
1531
+ finally {
1532
+ SkiaControl.invalidateOrigin = outer;
1533
+ }
1081
1534
  }
1082
1535
  // ---- gestures (same names as DrawnUi) ----
1083
1536
  /** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
@@ -1136,15 +1589,19 @@ export class SkiaControl {
1136
1589
  if (this.ConsumeGestures) {
1137
1590
  const sent = new SkiaGesturesInfo(args, apply);
1138
1591
  this.ConsumeGestures(this, sent);
1139
- if (args.Type !== "Up" && sent.Consumed)
1592
+ if (args.Type !== "Up" && sent.Consumed) {
1593
+ args.Event.Handled = true;
1140
1594
  return this;
1595
+ }
1141
1596
  }
1142
1597
  // C# EffectsGestureProcessors: attached effects that process gestures (ISkiaGestureProcessor) see them first
1143
1598
  if (!this.DisableEffects)
1144
1599
  for (const e of this.visualEffects) {
1145
1600
  const p = e.ProcessGestures;
1146
- if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up")
1601
+ if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up") {
1602
+ args.Event.Handled = true;
1147
1603
  return this;
1604
+ }
1148
1605
  }
1149
1606
  if (this.CheckChildrenGesturesLocked(args.Type))
1150
1607
  return consumedDefault;