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

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 +31 -25
  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 +2 -0
  10. package/dist/controls/SkiaBackdrop.js +2 -0
  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 +202 -7
  69. package/dist/core/SkiaControl.js +467 -55
  70. package/dist/core/SkiaEffect.d.ts +7 -0
  71. package/dist/core/SkiaEffect.js +25 -6
  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 +29 -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();
@@ -86,6 +104,14 @@ export class SkiaControl {
86
104
  /** Degrees. */
87
105
  SkewX = 0;
88
106
  SkewY = 0;
107
+ /** Degrees, a 3D turn around the horizontal axis through the pivot, seen with perspective (C# RotationX). */
108
+ RotationX = 0;
109
+ /** Degrees, a 3D turn around the vertical axis through the pivot (C# RotationY). */
110
+ RotationY = 0;
111
+ /** Degrees, a 3D turn around the axis facing the viewer, after RotationX and RotationY (C# RotationZ). */
112
+ RotationZ = 0;
113
+ /** Depth: toward the viewer (+, bigger) or away (−, smaller), turned or flat; draws at 576 / (576 − TranslationZ) (C# TranslationZ). */
114
+ TranslationZ = 0;
89
115
  /** Pivot for Rotation/Scale/Skew as a fraction of the box (MAUI default 0.5). */
90
116
  AnchorX = 0.5;
91
117
  AnchorY = 0.5;
@@ -113,8 +139,9 @@ export class SkiaControl {
113
139
  get Scale() { return this.ScaleX; }
114
140
  set Scale(v) { this.ScaleX = v; this.ScaleY = v; }
115
141
  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;
142
+ 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
143
  }
144
+ get Has3d() { return this.RotationX !== 0 || this.RotationY !== 0 || this.RotationZ !== 0 || this.TranslationZ !== 0; }
118
145
  /** Matrix applied at the last render (canvas space), undefined when none; gestures map through its inverse. */
119
146
  RenderTransformMatrix;
120
147
  /** Generic numeric parameters (DrawnUi Value1/Value2): SkiaShape Arc uses start angle / sweep angle in degrees. */
@@ -131,6 +158,7 @@ export class SkiaControl {
131
158
  return;
132
159
  this.bindingContext = value;
133
160
  this.OnBindingContextChanged();
161
+ this.Superview?.Hover?.RequestCheck(); // a recycled cell under a still mouse now shows another item
134
162
  }
135
163
  /** Index of the bound item inside its ItemsSource, -1 when not templated. */
136
164
  ContextIndex = -1;
@@ -163,15 +191,52 @@ export class SkiaControl {
163
191
  compositeSurface;
164
192
  /** Own content or structure changed: the next composite record is a full one. */
165
193
  compositeFull = true;
194
+ /** Where the kept composite surface starts relative to DrawingRect (the effects margin it was laid out with). */
195
+ compositeInsetX = 0;
196
+ compositeInsetY = 0;
166
197
  /** Canvas-pixel bounds this control covered when its parent last recorded it (composite erase region). */
167
198
  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: [] };
199
+ /**
200
+ * What the last composite record did (diagnostics): "full" or "partial", the number of children re-recorded and which
201
+ * ones; Areas: the pixels erased and drawn again for changes deeper than a child (C# Areas), Changed: the controls
202
+ * those areas belong to (C# Changed).
203
+ */
204
+ LastCompositeRecord = { Mode: "full", Children: 0, Dirty: [], Areas: [], Changed: [] };
205
+ /** C# MaxCompositionAreas: more areas than this in one composite record draw it whole instead. */
206
+ static MaxCompositionAreas = 16;
207
+ /** C# MaxCompositionShare: areas covering more than this share of a composite draw it whole instead. */
208
+ static MaxCompositionShare = 0.5;
209
+ /**
210
+ * Changes deeper than a direct child since the last composite record (drawnui-cross 6m): the control, the direct child
211
+ * it is in, where it was drawn before the change (its own drawn bounds, as drawn) and its rect then.
212
+ */
213
+ compositeDeep;
214
+ /** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
215
+ static drawOrigin = null;
170
216
  /** The children a composite record can re-record individually; layouts return their views. */
171
217
  GetCompositeChildren() { return []; }
172
218
  /** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
173
219
  TrackChildAsDirty(child) { if (this.IsCacheComposite)
174
220
  this.DirtyChildrenInternal.add(child); }
221
+ static rejectRect = new Float32Array(4);
222
+ /** Its cache must be recorded before it can be drawn (none, dirty, another type, scale, size or GPU context). */
223
+ CacheStale(scale) {
224
+ const cacheType = this.UsingCacheType;
225
+ if (cacheType === "None")
226
+ return false;
227
+ const o = this.RenderObject, r = cacheType === "Operations" ? this.ExpandedCacheRect(scale) : this.AlignedCacheRect(scale);
228
+ return this.cacheDirty || !o || o.Type !== cacheType || o.Epoch !== (this.Superview?.GpuEpoch ?? 0) || o.Scale !== scale
229
+ || Math.round(o.Bounds.Width) !== Math.round(r.Width) || Math.round(o.Bounds.Height) !== Math.round(r.Height);
230
+ }
231
+ /** Its rect grown by its effects margin, through the matrix it is drawn with, lies outside the canvas clip. */
232
+ OutsideClip(canvas, scale) {
233
+ const b = SkiaControl.MapRect(this.RenderTransformMatrix, this.ExpandedCacheRect(scale)), q = SkiaControl.rejectRect;
234
+ q[0] = b.Left;
235
+ q[1] = b.Top;
236
+ q[2] = b.Right;
237
+ q[3] = b.Bottom;
238
+ return canvas.quickReject(q);
239
+ }
175
240
  /** Drawn bounds in canvas pixels including transforms and effects margins (C# GetTransformedDirtyBounds). */
176
241
  GetTransformedDirtyBounds() {
177
242
  const r = this.ExpandedCacheRect(this.RenderingScale), m = this.RenderTransformMatrix;
@@ -190,6 +255,57 @@ export class SkiaControl {
190
255
  /** Consume every gesture that lands on this control so nothing below (z-order) receives it. */
191
256
  BlockGesturesBelow = false;
192
257
  LockChildrenGestures = "Disabled";
258
+ // ---- hover (drawnui-cross 6m, C# ReceivesHover / IsHovered / HoverChanged) ----
259
+ receivesHover;
260
+ /**
261
+ * Whether this control takes mouse hover: the canvas sets IsHovered while the mouse is over it. Opt-in: false by
262
+ * default, true by default only in the controls that always had hover (SkiaButton, SkiaSlider, the toggles,
263
+ * SkiaRadioButton, SkiaCarousel, SkiaDrawer), never because a control has a tap handler. A HoverChanged handler turns
264
+ * it on (C# fluent OnHovered) unless it is set.
265
+ */
266
+ get ReceivesHover() { return this.receivesHover ?? (this.HoverChanged ? true : this.ReceivesHoverByDefault()); }
267
+ set ReceivesHover(value) { this.receivesHover = value; }
268
+ /** The value of ReceivesHover until it is set. */
269
+ ReceivesHoverByDefault() { return false; }
270
+ isHovered = false;
271
+ /**
272
+ * True while the mouse is over this control and it takes hover. Every such control under the pointer is hovered at
273
+ * once (a card and the button inside it, as CSS :hover); it stays as it is while content moves under the pointer and
274
+ * is checked again when that stops. Touch never hovers.
275
+ */
276
+ get IsHovered() { return this.isHovered; }
277
+ /** IsHovered changed: true when the mouse came over this control, false when it left (C# HoverChanged / OnHovered). */
278
+ HoverChanged;
279
+ /** Set by the canvas's HoverManager. */
280
+ ApplyHover(on) {
281
+ if (this.isHovered === on)
282
+ return;
283
+ this.isHovered = on;
284
+ this.HoverChanged?.(this, on);
285
+ }
286
+ /** A scroll, carousel or drawer moving its content right now: hover waits until it stops (C# IsScrolling / InTransition). */
287
+ MovesContent() { return false; }
288
+ /**
289
+ * Hover hit path (HoverManager): this control is under the point (in its own space), its children under it follow,
290
+ * top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
291
+ * controls under it get no hover (a popup over a list).
292
+ */
293
+ CollectHovered(point, into, xs, ys) {
294
+ into.push(this);
295
+ xs?.push(point.X);
296
+ ys?.push(point.Y);
297
+ if (!this.CheckChildrenGesturesLocked("Pointer")) {
298
+ const listeners = this.GetGestureListeners();
299
+ for (let i = listeners.length - 1; i >= 0; i--) {
300
+ const child = listeners[i];
301
+ if (!child.IsVisible || child.InputTransparent || !this.IsGestureForChild(child, point))
302
+ continue;
303
+ if (child.CollectHovered(child.TransformPointToLocalSpace(point), into, xs, ys))
304
+ return true;
305
+ }
306
+ }
307
+ return this.BlockGesturesBelow;
308
+ }
193
309
  // ---- touch feedback (same names as DrawnUi) ----
194
310
  TouchEffectColor = Colors.White;
195
311
  /** Plays a ripple (from the tap point) when Tapped fires. */
@@ -198,6 +314,11 @@ export class SkiaControl {
198
314
  AnimationTappedSpeed = 0;
199
315
  /** Overlay effects drawn above this control's content every frame (ripple etc). */
200
316
  PostAnimators = [];
317
+ /**
318
+ * C# PendingUnattachedAnimators: animators started while no Canvas was above this control yet (e.g. a game loop
319
+ * started in a constructor). They start on the control's first arrange, where C# starts them in SuperViewChanged.
320
+ */
321
+ PendingUnattachedAnimators;
201
322
  // ---- visual effects (C# VisualEffects: attached SkiaEffect objects) ----
202
323
  visualEffects = [];
203
324
  /** Post-renderer effects among VisualEffects (C# EffectPostRenderers). */
@@ -229,10 +350,18 @@ export class SkiaControl {
229
350
  return [];
230
351
  return this.EffectPostRenderers.filter((e) => e.NeedApply);
231
352
  }
232
- /** C# CachedImage: the cached texture and the canvas rect it was rasterized over (Image caches only). */
353
+ /**
354
+ * C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
355
+ * re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
356
+ * at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
357
+ * there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
358
+ */
233
359
  get CachedImage() {
234
360
  const c = this.RenderObject;
235
- return c?.Image ? { Image: c.Image, Bounds: c.Bounds } : undefined;
361
+ if (!c?.Image)
362
+ return undefined;
363
+ const r = this.AlignedCacheRect(c.Scale);
364
+ return { Image: c.Image, Bounds: SKRect.Create(r.Left, r.Top, c.Bounds.Width, c.Bounds.Height) };
236
365
  }
237
366
  /** Clip overlay effects to the control's shape (CreateClip). */
238
367
  ClipEffects = true;
@@ -250,6 +379,22 @@ export class SkiaControl {
250
379
  // ---- tree ----
251
380
  Parent;
252
381
  /** Containers override; a leaf control cannot host children. */
382
+ styledInitially = false;
383
+ /**
384
+ * C# ApplyInitialStyles: takes the property defaults registered with `ConfigureStyles` for this control's class.
385
+ * The reconciler calls it with `atConstruction` right after `new`, before the JSX props, so props win; a control
386
+ * built in code-behind is styled on its first measure and keeps whatever the code already set.
387
+ */
388
+ ApplyInitialStyles(atConstruction = false) {
389
+ this.styledInitially = true;
390
+ if (!SkiaStyles.IsEmpty)
391
+ SkiaStyles.Apply(this, atConstruction);
392
+ }
393
+ /**
394
+ * C# IsChildrenItem: added by the app (JSX children or the `Children` list), not a part the control made itself. A new
395
+ * `Children` list drops only these, so a control's own parts keep their place.
396
+ */
397
+ IsChildrenItem = false;
253
398
  AddSubView(_control) { throw new Error(`DrawnUi: ${this.constructor.name} cannot host children`); }
254
399
  InsertSubView(_index, control) { this.AddSubView(control); }
255
400
  RemoveSubView(_control) { }
@@ -271,6 +416,8 @@ export class SkiaControl {
271
416
  * this is what keeps a cached tree from re-measuring text every frame.
272
417
  */
273
418
  Measure(widthConstraint, heightConstraint, scale) {
419
+ if (!this.styledInitially)
420
+ this.ApplyInitialStyles(false);
274
421
  if (!this.NeedMeasure && this.RenderingScale === scale
275
422
  && Object.is(this.lastWidthConstraint, widthConstraint) && Object.is(this.lastHeightConstraint, heightConstraint)) {
276
423
  return this.MeasuredSize;
@@ -347,12 +494,37 @@ export class SkiaControl {
347
494
  const fullW = destination.Width - m.HorizontalThickness * scale, fullH = destination.Height - m.VerticalThickness * scale;
348
495
  const x = availL + SkiaControl.Align(this.HorizontalOptions, fullW, w);
349
496
  const y = availT + SkiaControl.Align(this.VerticalOptions, fullH, h);
497
+ const old = this.DrawingRect;
350
498
  this.DrawingRect = SKRect.Create(x, y, w, h);
499
+ this.FollowMatrix(old);
351
500
  this.OnLayoutChanged();
501
+ if (this.PendingUnattachedAnimators)
502
+ this.StartPendingAnimators();
352
503
  // DrawnUi OnLayoutReady: an accessible control registers itself on its first layout
353
504
  if (!this.registeredWithAccessibility && this.IsAccessibilityElement)
354
505
  this.NotifyAccessibility();
355
506
  }
507
+ /**
508
+ * A cached ancestor replays its picture where a re-arrange moved it (a scroll) without rendering this control again,
509
+ * so the matrix made at its last Render moves with its rect: taps, hover and the accessibility rect stay on what is
510
+ * drawn (C# offsets the cached render tree instead). A control that renders again gets a fresh matrix there.
511
+ */
512
+ FollowMatrix(old) {
513
+ const m = this.RenderTransformMatrix, r = this.DrawingRect;
514
+ if (!m || (old.Left === r.Left && old.Top === r.Top))
515
+ return;
516
+ const M = Super.CK.Matrix, dx = r.Left - old.Left, dy = r.Top - old.Top;
517
+ this.RenderTransformMatrix = M.multiply(M.translated(dx, dy), m, M.translated(-dx, -dy));
518
+ }
519
+ /** C# SuperViewChanged: starts the animators queued before this control was attached. */
520
+ StartPendingAnimators() {
521
+ if (!this.Superview)
522
+ return;
523
+ const pending = this.PendingUnattachedAnimators;
524
+ this.PendingUnattachedAnimators = undefined;
525
+ for (const animator of pending)
526
+ animator.Start();
527
+ }
356
528
  // ---- accessibility (DrawnUi ISkiaAccessibilityNode) ----
357
529
  static nextAccessibilityId = 1;
358
530
  /** Stable identity for the DOM overlay. */
@@ -387,8 +559,12 @@ export class SkiaControl {
387
559
  this.accessibilityHint = v;
388
560
  this.AccessibilityChanged();
389
561
  } }
390
- /** Tab stop + activation; unset = true when the control handles Tapped. */
391
- get AccessibilityCanInteract() { return this.accessibilityCanInteract ?? this.DefaultAccessibilityCanInteract(); }
562
+ /**
563
+ * Whether keyboard navigation and assistive technology may focus and use this node: the explicit value, else the class
564
+ * default (true when the control handles Tapped), and in both cases only while a tap could reach the control
565
+ * (CanReceiveGesture), so Tab, Enter / Space and screen-reader activation never use a control the pointer cannot.
566
+ */
567
+ get AccessibilityCanInteract() { return (this.accessibilityCanInteract ?? this.DefaultAccessibilityCanInteract()) && this.CanReceiveGesture("Tapped"); }
392
568
  set AccessibilityCanInteract(v) { if (this.accessibilityCanInteract !== v) {
393
569
  this.accessibilityCanInteract = v;
394
570
  this.AccessibilityChanged();
@@ -399,6 +575,8 @@ export class SkiaControl {
399
575
  * host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
400
576
  */
401
577
  WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
578
+ /** A range control's value for screen readers (slider, progress); undefined for everything else (drawnui-cross 6c). */
579
+ get AccessibilityValue() { return undefined; }
402
580
  /** aria-pressed for toggles; undefined = not a toggle. */
403
581
  get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
404
582
  set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
@@ -473,6 +651,39 @@ export class SkiaControl {
473
651
  }
474
652
  /** Overlay focus arrives on / leaves this node (inputs may activate their sink here). */
475
653
  OnAccessibilityFocused(_focused) { }
654
+ /**
655
+ * Keyboard navigation hands this control the keys it may use while it holds keyboard focus (ArrowLeft / Right / Up /
656
+ * Down, Home, End, PageUp, PageDown, DOM `event.code` names). Return true when the key was used; the default uses none,
657
+ * so the key moves focus inside the arrow-key group around the control.
658
+ */
659
+ OnAccessibilityKey(_key) { return false; }
660
+ /**
661
+ * Whether a pointer gesture of this kind would reach this control, by the rules the gesture dispatch applies: the
662
+ * control and every ancestor draw, none of them is InputTransparent, no ancestor keeps this gesture from its children
663
+ * (LockChildrenGestures, PassNone included) and the control accepts input (AcceptsInput). Opacity does not count, as
664
+ * for the pointer. Keyboard navigation uses it: Tapped for Tab and Enter / Space, Panning for the arrow keys.
665
+ */
666
+ CanReceiveGesture(gesture) {
667
+ if (!this.IsVisible || this.IsDisposed || this.InputTransparent || !this.AcceptsInput())
668
+ return false;
669
+ for (let p = this.Parent; p; p = p.Parent)
670
+ if (!p.IsVisible || p.IsDisposed || p.InputTransparent || p.CheckChildrenGesturesLocked(gesture))
671
+ return false;
672
+ return true;
673
+ }
674
+ /**
675
+ * False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
676
+ * Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
677
+ */
678
+ AcceptsInput() { return true; }
679
+ /**
680
+ * What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
681
+ * press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
682
+ * it (a scroll behind it still scrolls).
683
+ */
684
+ BlockedGestureResult(args) {
685
+ return args.Type === "Down" || args.Type === "Tapped" || args.Type === "LongPressing" ? this : null;
686
+ }
476
687
  NotifyAccessibilityFocused(focused) { this.Superview?.AccessibilityManager.NotifyFocused(focused ? this : undefined); }
477
688
  /** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
478
689
  static SmartMax(a, b) {
@@ -538,8 +749,25 @@ export class SkiaControl {
538
749
  const applyOpacity = this.Opacity < 1;
539
750
  // C# Left/Top: a cached control is blitted at an offset, no matrix and no save/restore around the subtree
540
751
  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;
752
+ && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !this.Has3d;
542
753
  const needTransform = this.HasTransform && !offsetOnly;
754
+ let dx = 0, dy = 0;
755
+ if (needTransform) {
756
+ this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
757
+ }
758
+ else if (offsetOnly) {
759
+ dx = this.Left * ctx.Scale;
760
+ dy = this.Top * ctx.Scale;
761
+ this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
762
+ }
763
+ else {
764
+ this.RenderTransformMatrix = undefined;
765
+ }
766
+ // drawn entirely outside the clip: skipped with its subtree (C# layout Virtualisation, DrawnUi.Rust); the matrix
767
+ // above stays current for hit tests. A cache that must be recorded is still recorded here, as before, so its cost
768
+ // does not move into the frame where the control scrolls in (DrawnUi.Rust records it then)
769
+ if (this.OutsideClip(canvas, ctx.Scale) && !this.CacheStale(ctx.Scale))
770
+ return;
543
771
  let saved = false;
544
772
  // same as DrawnUi: opacity = a layer with alpha, transforms = canvas matrix around the whole subtree (cache included)
545
773
  if (applyOpacity) {
@@ -553,19 +781,8 @@ export class SkiaControl {
553
781
  canvas.save();
554
782
  saved = true;
555
783
  }
556
- let dx = 0, dy = 0;
557
- if (needTransform) {
558
- this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
784
+ if (needTransform)
559
785
  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
786
  if (this.IsClippedToBounds) {
570
787
  if (!saved) {
571
788
  canvas.save();
@@ -578,11 +795,12 @@ export class SkiaControl {
578
795
  if (saved)
579
796
  canvas.restore();
580
797
  }
581
- /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · T(pivot) · translation, in canvas pixels. */
798
+ /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · 3D · T(pivot) · translation, in canvas pixels. */
582
799
  CreateRenderTransformMatrix(destination, scale) {
583
800
  const M = Super.CK.Matrix;
584
801
  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)
802
+ const has3d = this.Has3d;
803
+ if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !has3d)
586
804
  return M.translated(moveX, moveY);
587
805
  const px = destination.Left + destination.Width * this.AnchorX;
588
806
  const py = destination.Top + destination.Height * this.AnchorY;
@@ -591,7 +809,36 @@ export class SkiaControl {
591
809
  const scaleSkew = [this.ScaleX, kx, 0, ky, this.ScaleY, 0, 0, 0, 1];
592
810
  const rotation = this.Rotation !== 0 ? M.rotated((Math.PI * this.Rotation) / 180) : M.identity();
593
811
  // 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));
812
+ if (!has3d)
813
+ return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
814
+ 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));
815
+ }
816
+ /** C# camera distance of the 3D transforms (Sk3dView.CameraDistance): 8 inches at 72 dpi, in canvas pixels. */
817
+ static CameraDistance = 576;
818
+ /**
819
+ * C# Create3dMatrix (Sk3dView): RotationX, then RotationY, then RotationZ (negated) around the pivot, projected from
820
+ * the camera; TranslationZ is depth, a flat control at depth z draws at camera / (camera − z).
821
+ */
822
+ static Create3dMatrix(rotationX, rotationY, rotationZ, translationZ) {
823
+ const d = Math.PI / 180, camera = SkiaControl.CameraDistance;
824
+ const sx = Math.sin(rotationX * d), cx = Math.cos(rotationX * d), sy = Math.sin(rotationY * d), cy = Math.cos(rotationY * d);
825
+ const sz = Math.sin(-rotationZ * d), cz = Math.cos(-rotationZ * d);
826
+ return [
827
+ cy * cz, cy * sz, 0,
828
+ sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, 0,
829
+ (cx * sy * cz + sx * sz) / camera, (cx * sy * sz - sx * cz) / camera, Math.max(camera - translationZ, 1) / camera,
830
+ ];
831
+ }
832
+ /** C# SkiaSamplingOptions.IsMoveOnly: the matrix only moves (or mirrors), so a bitmap keeps its pixels 1:1. */
833
+ static IsMoveOnly(m) {
834
+ 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;
835
+ }
836
+ /**
837
+ * C# WillDrawDistorted: what this control draws here ends up scaled, turned, skewed or projected, by its own transforms
838
+ * or a live ancestor's (both in the canvas matrix) or by the picture it is recorded into. Images then sample smoothly.
839
+ */
840
+ WillDrawDistorted(ctx) {
841
+ return !!ctx.Context.PlaybackDistorted || !SkiaControl.IsMoveOnly(ctx.Context.Canvas.getTotalMatrix());
595
842
  }
596
843
  /** Maps a point given in the parent's space into this control's untransformed space (DrawnUi TransformPointToLocalSpace). */
597
844
  TransformPointToLocalSpace(point) {
@@ -627,26 +874,35 @@ export class SkiaControl {
627
874
  }
628
875
  else {
629
876
  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)
877
+ const epoch = this.Superview?.GpuEpoch ?? 0;
878
+ if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
879
+ this.DisposePrevious(); // made on a lost GPU context
880
+ // a picture replays its image blits with the sampling chosen when it was recorded: recorded again once its
881
+ // playback turns distorted or straight (C# SamplingMismatch), else a cached child of a turning card stays jagged
882
+ const o = this.RenderObject;
883
+ if (this.CacheStale(ctx.Scale) || (o?.Picture && o.HasImageBlits && o.PlaybackDistorted !== this.WillDrawDistorted(ctx)))
634
884
  this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
635
885
  if (this.RenderObject) {
636
886
  // C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
637
887
  // and paint the result; a picture cache has no texture, so it is replayed first and snapshotted by the effect
638
888
  if (post.length === 0 || !this.RenderObject.Image)
639
- this.RenderObject.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy);
889
+ this.DrawCache(this.RenderObject, ctx, r.Left + dx, r.Top + dy);
640
890
  for (const e of post)
641
891
  e.Render(own);
642
892
  }
643
893
  else if (this.RenderObjectPrevious)
644
- this.RenderObjectPrevious.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy); // double buffer: last good frame
894
+ this.DrawCache(this.RenderObjectPrevious, ctx, r.Left + dx, r.Top + dy); // double buffer: last good frame
645
895
  else
646
896
  this.DrawPlaceholder(own);
647
897
  }
648
898
  this.ExecutePostAnimators(own);
649
899
  }
900
+ /** Blits a cache: an image samples smoothly when drawn distorted; a picture being recorded learns it holds image blits. */
901
+ DrawCache(cache, ctx, left, top) {
902
+ if (cache.Image || cache.HasImageBlits)
903
+ ctx.Context.HasImageBlits = true;
904
+ cache.Draw(ctx.Context.Canvas, left, top, !!cache.Image && this.WillDrawDistorted(ctx));
905
+ }
650
906
  /** Background + Paint(): the part of the control that a cache captures. */
651
907
  PaintContent(ctx) {
652
908
  if (this.BackgroundColor || (this.FillGradient && this.FillGradientPaintsBackground()) || this.PaintsBackgroundWithoutColor())
@@ -681,11 +937,16 @@ export class SkiaControl {
681
937
  }
682
938
  if (cacheType === "Operations") {
683
939
  const recorder = new CK.PictureRecorder();
940
+ const playbackDistorted = this.WillDrawDistorted(ctx);
684
941
  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 } });
942
+ const recording = { ...ctx.Context, Canvas: canvas, Recording: true, PlaybackDistorted: playbackDistorted, HasImageBlits: false };
943
+ this.PaintContent({ ...ctx, Context: recording });
686
944
  const picture = recorder.finishRecordingAsPicture();
687
945
  recorder.delete();
688
946
  this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
947
+ this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
948
+ this.RenderObject.PlaybackDistorted = playbackDistorted;
949
+ this.RenderObject.HasImageBlits = recording.HasImageBlits;
689
950
  }
690
951
  else if (cacheType === "ImageComposite") {
691
952
  this.CreateCompositeRenderingObject(ctx, r, w, h);
@@ -709,6 +970,7 @@ export class SkiaControl {
709
970
  const image = offscreen.makeImageSnapshot();
710
971
  offscreen.delete();
711
972
  this.RenderObject = new CachedObject(cacheType, r, ctx.Scale, undefined, image);
973
+ this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
712
974
  }
713
975
  this.cacheDirty = false;
714
976
  }
@@ -727,7 +989,9 @@ export class SkiaControl {
727
989
  }
728
990
  let surface = this.compositeSurface;
729
991
  const prev = this.RenderObject;
730
- const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale
992
+ const epoch = this.Superview?.GpuEpoch ?? 0;
993
+ // the kept surface belongs to the GPU context it was made on: after a context restore it is made again
994
+ const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale && prev.Epoch === epoch
731
995
  && Math.round(prev.Bounds.Width) === w && Math.round(prev.Bounds.Height) === h;
732
996
  if (!sameGeometry) {
733
997
  surface?.delete();
@@ -740,14 +1004,46 @@ export class SkiaControl {
740
1004
  }
741
1005
  const canvas = surface.getCanvas();
742
1006
  const children = this.GetCompositeChildren();
743
- const partial = sameGeometry && !this.compositeFull && this.DirtyChildrenInternal.size > 0 && children.length > 0;
1007
+ const deep = this.compositeDeep;
1008
+ // the same size with another effects margin moves the content inside the surface: record whole (C# 97683f19)
1009
+ const insetX = r.Left - this.DrawingRect.Left, insetY = r.Top - this.DrawingRect.Top;
1010
+ const sameInset = insetX === this.compositeInsetX && insetY === this.compositeInsetY;
1011
+ this.compositeInsetX = insetX;
1012
+ this.compositeInsetY = insetY;
1013
+ let partial = sameGeometry && sameInset && !this.compositeFull && children.length > 0 && (this.DirtyChildrenInternal.size > 0 || !!deep?.length);
1014
+ // changes deeper than a child: only their areas, the child they are in drawn clipped to them (drawnui-cross 6m)
1015
+ const areas = [], changed = [], inside = new Set();
1016
+ if (partial && deep?.length) {
1017
+ let covered = 0;
1018
+ for (const d of deep) {
1019
+ if (!children.includes(d.Child) || this.DirtyChildrenInternal.has(d.Child))
1020
+ continue;
1021
+ const area = SkiaControl.DeepArea(d, this, ctx.Scale);
1022
+ if (area === null) {
1023
+ partial = false;
1024
+ break;
1025
+ }
1026
+ if (!area) {
1027
+ this.DirtyChildrenInternal.add(d.Child);
1028
+ continue;
1029
+ }
1030
+ areas.push(area);
1031
+ changed.push(d.Origin);
1032
+ inside.add(d.Child);
1033
+ covered += area.Width * area.Height;
1034
+ }
1035
+ if (areas.length > SkiaControl.MaxCompositionAreas || covered > r.Width * r.Height * SkiaControl.MaxCompositionShare)
1036
+ partial = false;
1037
+ }
1038
+ if (deep)
1039
+ deep.length = 0;
744
1040
  const offset = prev && sameGeometry ? { X: r.Left - prev.Bounds.Left, Y: r.Top - prev.Bounds.Top } : { X: 0, Y: 0 };
745
1041
  const saved = canvas.save();
746
1042
  canvas.translate(-r.Left, -r.Top);
747
1043
  if (partial) {
748
1044
  // dirty = reported children + siblings intersecting their old or new bounds (C# makes intersecting children dirty too)
749
1045
  const dirty = new Set();
750
- const rects = [];
1046
+ const rects = [...areas];
751
1047
  const boundsOf = (c) => {
752
1048
  const out = [c.GetTransformedDirtyBounds()];
753
1049
  if (c.lastCompositeBounds)
@@ -763,7 +1059,7 @@ export class SkiaControl {
763
1059
  while (grew) {
764
1060
  grew = false;
765
1061
  for (const c of children) {
766
- if (dirty.has(c) || !c.IsVisible)
1062
+ if (dirty.has(c) || inside.has(c) || !c.IsVisible)
767
1063
  continue;
768
1064
  const own = boundsOf(c);
769
1065
  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 +1086,18 @@ export class SkiaControl {
790
1086
  this.DirtyChildrenInternal.clear();
791
1087
  for (const c of dirty)
792
1088
  this.DirtyChildrenInternal.add(c);
1089
+ for (const c of inside)
1090
+ this.DirtyChildrenInternal.add(c);
1091
+ // children entirely outside the redrawn areas are skipped by their own clip check (Render)
793
1092
  this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
794
1093
  this.IsRenderingWithComposition = false;
795
- this.LastCompositeRecord = { Mode: "partial", Children: dirty.size, Dirty: [...dirty] };
1094
+ const redrawn = children.filter((c) => this.DirtyChildrenInternal.has(c));
1095
+ this.LastCompositeRecord = { Mode: "partial", Children: redrawn.length, Dirty: redrawn, Areas: areas, Changed: changed };
796
1096
  }
797
1097
  else {
798
1098
  canvas.clear(CK.TRANSPARENT);
799
1099
  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 };
1100
+ this.LastCompositeRecord = { Mode: "full", Children: children.length, Dirty: children, Areas: [], Changed: [] };
801
1101
  }
802
1102
  canvas.restoreToCount(saved);
803
1103
  for (const c of children)
@@ -807,6 +1107,7 @@ export class SkiaControl {
807
1107
  const image = surface.makeImageSnapshot();
808
1108
  const old = this.RenderObject;
809
1109
  this.RenderObject = new CachedObject("ImageComposite", r, ctx.Scale, undefined, image);
1110
+ this.RenderObject.Epoch = epoch;
810
1111
  if (old) {
811
1112
  const sv = this.Superview;
812
1113
  if (sv)
@@ -842,7 +1143,11 @@ export class SkiaControl {
842
1143
  /** Marks the cache stale; re-recorded on the next frame (a composite records fully: its own content changed). */
843
1144
  InvalidateCache() { this.cacheDirty = true; this.compositeFull = true; }
844
1145
  /** Effects margin recomputed on the next use (C# InvalidateEffectsMargin). */
845
- InvalidateEffectsMargin() { this.effectsMarginCache = undefined; }
1146
+ InvalidateEffectsMargin() {
1147
+ // a layout's margin aggregates what its children paint outside it: every ancestor recomputes too (C# 97683f19)
1148
+ for (let c = this; c; c = c.Parent)
1149
+ c.effectsMarginCache = undefined;
1150
+ }
846
1151
  /** Draws PostAnimators above content; an effect returning true asks for another frame. */
847
1152
  ExecutePostAnimators(ctx) {
848
1153
  if (this.PostAnimators.length === 0)
@@ -1024,6 +1329,7 @@ export class SkiaControl {
1024
1329
  this.ownAnimations.clear();
1025
1330
  for (const e of [...this.PostAnimators])
1026
1331
  e.Stop?.();
1332
+ this.PendingUnattachedAnimators = undefined;
1027
1333
  this.PostAnimators.length = 0;
1028
1334
  this.DestroyRenderingObject();
1029
1335
  this.compositeSurface?.delete();
@@ -1052,32 +1358,134 @@ export class SkiaControl {
1052
1358
  }
1053
1359
  /** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
1054
1360
  Repaint() {
1055
- this.Superview?.Update();
1361
+ if (!this.UnderHiddenParent())
1362
+ this.Superview?.Update();
1363
+ }
1364
+ /**
1365
+ * A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
1366
+ * parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
1367
+ * change still draws: it is the one that hides or shows it.
1368
+ */
1369
+ UnderHiddenParent() {
1370
+ for (let p = this.Parent; p; p = p.Parent)
1371
+ if (!p.IsVisible)
1372
+ return true;
1373
+ return false;
1056
1374
  }
1375
+ /** The control or a parent is hidden: its animators pause until it shows again. */
1376
+ HiddenInTree() { return !this.IsVisible || this.UnderHiddenParent(); }
1377
+ /** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
1378
+ static invalidateOrigin = null;
1057
1379
  /**
1058
1380
  * Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
1059
1381
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
1060
1382
  */
1061
1383
  RepaintComposition() {
1062
- let child = this, p = this.Parent;
1063
- while (p) {
1384
+ this.ReportToComposites();
1385
+ for (let p = this.Parent; p; p = p.Parent)
1064
1386
  p.cacheDirty = true;
1065
- p.TrackChildAsDirty(child);
1066
- child = p;
1067
- p = p.Parent;
1068
- }
1069
1387
  this.Repaint();
1070
1388
  }
1389
+ /**
1390
+ * Update() for a change of how this control looks, not of its size (a color set through props, C# NeedDraw): the same
1391
+ * re-measure and cache invalidation, but an ImageComposite above redraws only this control's area instead of
1392
+ * recording whole (drawnui-cross 6m). The record still goes whole when the control's rect changed after all.
1393
+ */
1394
+ UpdateDraw() {
1395
+ this.ReportToComposites();
1396
+ const outer = SkiaControl.drawOrigin;
1397
+ SkiaControl.drawOrigin = this;
1398
+ try {
1399
+ this.InvalidateMeasure();
1400
+ }
1401
+ finally {
1402
+ SkiaControl.drawOrigin = outer;
1403
+ }
1404
+ }
1405
+ /**
1406
+ * A change of this control that keeps its layout, to every ImageComposite above: a direct child is drawn again whole
1407
+ * (C# TrackChildAsDirty), a deeper control by its area, read now as it was drawn (its last matrix and effects margin).
1408
+ */
1409
+ ReportToComposites() {
1410
+ let before;
1411
+ let child = this;
1412
+ for (let p = this.Parent; p; child = p, p = p.Parent) {
1413
+ if (!p.IsCacheComposite)
1414
+ continue;
1415
+ if (child === this) {
1416
+ p.TrackChildAsDirty(child);
1417
+ continue;
1418
+ }
1419
+ const deep = p.compositeDeep ??= [];
1420
+ if (deep.some((d) => d.Origin === this))
1421
+ continue;
1422
+ before ??= this.GetTransformedDirtyBounds();
1423
+ const r = this.DrawingRect;
1424
+ deep.push({ Origin: this, Child: child, Before: before, Rect: new SKRect(r.Left, r.Top, r.Right, r.Bottom) });
1425
+ }
1426
+ }
1427
+ /** The matrix this control will be drawn with (Render), from its current properties; undefined when none. */
1428
+ CurrentMatrix(scale) {
1429
+ return this.HasTransform ? this.CreateRenderTransformMatrix(this.DrawingRect, scale) : undefined;
1430
+ }
1431
+ /** Where this control will be drawn: its rect grown by its effects margin, through its current matrix. */
1432
+ DrawnBoundsNow(scale) { return SkiaControl.MapRect(this.CurrentMatrix(scale), this.ExpandedCacheRect(scale)); }
1433
+ static MapRect(m, r) {
1434
+ if (!m)
1435
+ return r;
1436
+ const pts = Super.CK.Matrix.mapPoints(m, [r.Left, r.Top, r.Right, r.Top, r.Right, r.Bottom, r.Left, r.Bottom]);
1437
+ 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]));
1438
+ }
1439
+ /** Its output depends on all of its content (a visual effect; a backdrop samples below): a change inside redraws it whole. */
1440
+ get SamplesContent() { return !this.DisableEffects && this.visualEffects.length > 0; }
1441
+ /**
1442
+ * Where a deep change shows inside the composite (drawnui-cross 6m rule b): the control as drawn before and as drawn
1443
+ * now, then through the transform of every ancestor up to the composite. undefined: draw the child whole (an effect
1444
+ * or a backdrop on the way, the control gone from the child); null: the layout changed, record whole.
1445
+ */
1446
+ static DeepArea(d, composite, scale) {
1447
+ const o = d.Origin, r = o.DrawingRect;
1448
+ if (r.Left !== d.Rect.Left || r.Top !== d.Rect.Top || r.Right !== d.Rect.Right || r.Bottom !== d.Rect.Bottom)
1449
+ return null;
1450
+ if (!o.IsVisible)
1451
+ return undefined;
1452
+ const now = o.DrawnBoundsNow(scale);
1453
+ 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));
1454
+ let p = o.Parent, child = o;
1455
+ for (; p && p !== composite; child = p, p = p.Parent) {
1456
+ if (p.SamplesContent || !p.IsVisible)
1457
+ return undefined;
1458
+ area = SkiaControl.MapRect(p.CurrentMatrix(scale), area);
1459
+ }
1460
+ return p === composite && child === d.Child ? area : undefined;
1461
+ }
1462
+ /**
1463
+ * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
1464
+ * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when
1465
+ * NeedMeasure is set. Used by SkiaScroll for its scroll bars.
1466
+ */
1467
+ IsParentIndependent = false;
1071
1468
  /** Size or content may have changed: this control and all ancestors remeasure and re-record. */
1072
1469
  InvalidateMeasure() {
1073
1470
  this.NeedMeasure = true;
1074
1471
  this.cacheDirty = true;
1075
- this.compositeFull = true; // structure may change: a composite cannot patch it
1472
+ // structure may change: a composite cannot patch it; a draw-only change (UpdateDraw) reported its area instead
1473
+ if (SkiaControl.drawOrigin === null || SkiaControl.drawOrigin === this)
1474
+ this.compositeFull = true;
1076
1475
  this.effectsMarginCache = undefined;
1077
- if (this.Parent)
1078
- this.Parent.InvalidateMeasure();
1079
- else
1080
- this.Superview?.Update();
1476
+ const outer = SkiaControl.invalidateOrigin;
1477
+ SkiaControl.invalidateOrigin ??= this;
1478
+ try {
1479
+ if (this.Parent && this.IsParentIndependent)
1480
+ this.RepaintComposition();
1481
+ else if (this.Parent)
1482
+ this.Parent.InvalidateMeasure();
1483
+ else if (!SkiaControl.invalidateOrigin.UnderHiddenParent())
1484
+ this.Superview?.Update();
1485
+ }
1486
+ finally {
1487
+ SkiaControl.invalidateOrigin = outer;
1488
+ }
1081
1489
  }
1082
1490
  // ---- gestures (same names as DrawnUi) ----
1083
1491
  /** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
@@ -1136,15 +1544,19 @@ export class SkiaControl {
1136
1544
  if (this.ConsumeGestures) {
1137
1545
  const sent = new SkiaGesturesInfo(args, apply);
1138
1546
  this.ConsumeGestures(this, sent);
1139
- if (args.Type !== "Up" && sent.Consumed)
1547
+ if (args.Type !== "Up" && sent.Consumed) {
1548
+ args.Event.Handled = true;
1140
1549
  return this;
1550
+ }
1141
1551
  }
1142
1552
  // C# EffectsGestureProcessors: attached effects that process gestures (ISkiaGestureProcessor) see them first
1143
1553
  if (!this.DisableEffects)
1144
1554
  for (const e of this.visualEffects) {
1145
1555
  const p = e.ProcessGestures;
1146
- if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up")
1556
+ if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up") {
1557
+ args.Event.Handled = true;
1147
1558
  return this;
1559
+ }
1148
1560
  }
1149
1561
  if (this.CheckChildrenGesturesLocked(args.Type))
1150
1562
  return consumedDefault;