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.
- package/README.md +17 -4
- package/SKIPPED.md +33 -27
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +40 -0
- package/dist/controls/DrawnGame.js +64 -0
- package/dist/controls/GridStructure.d.ts +16 -2
- package/dist/controls/GridStructure.js +40 -4
- package/dist/controls/SkiaBackdrop.d.ts +8 -2
- package/dist/controls/SkiaBackdrop.js +48 -8
- package/dist/controls/SkiaButton.d.ts +19 -2
- package/dist/controls/SkiaButton.js +41 -11
- package/dist/controls/SkiaCarousel.d.ts +9 -1
- package/dist/controls/SkiaCarousel.js +16 -3
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaDrawer.d.ts +2 -0
- package/dist/controls/SkiaDrawer.js +2 -0
- package/dist/controls/SkiaEditor.d.ts +12 -1
- package/dist/controls/SkiaEditor.js +46 -19
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +12 -2
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +54 -3
- package/dist/controls/SkiaLabel.js +235 -52
- package/dist/controls/SkiaLabelFps.d.ts +16 -0
- package/dist/controls/SkiaLabelFps.js +41 -0
- package/dist/controls/SkiaLayout.d.ts +64 -7
- package/dist/controls/SkiaLayout.js +316 -19
- package/dist/controls/SkiaLottie.js +4 -2
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +6 -3
- package/dist/controls/SkiaRadioButton.js +4 -3
- package/dist/controls/SkiaScroll.d.ts +35 -5
- package/dist/controls/SkiaScroll.js +233 -23
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +20 -2
- package/dist/controls/SkiaSlider.js +59 -5
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaSvg.js +1 -1
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +25 -5
- package/dist/controls/SkiaToggle.js +58 -5
- package/dist/controls/SnappingLayout.d.ts +12 -1
- package/dist/controls/SnappingLayout.js +19 -1
- package/dist/core/Accessibility.d.ts +80 -0
- package/dist/core/Accessibility.js +365 -8
- package/dist/core/Animators.d.ts +37 -1
- package/dist/core/Animators.js +102 -9
- package/dist/core/Canvas.d.ts +108 -7
- package/dist/core/Canvas.js +363 -35
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +48 -2
- package/dist/core/Gestures.js +95 -3
- package/dist/core/Hover.d.ts +47 -0
- package/dist/core/Hover.js +134 -0
- package/dist/core/KeyboardManager.d.ts +13 -0
- package/dist/core/KeyboardManager.js +18 -0
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +216 -7
- package/dist/core/SkiaControl.js +516 -59
- package/dist/core/SkiaEffect.d.ts +17 -0
- package/dist/core/SkiaEffect.js +43 -8
- package/dist/core/SkiaImageManager.js +1 -1
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +17 -1
- package/dist/core/Super.js +35 -4
- package/dist/core/ViewsAdapter.d.ts +7 -2
- package/dist/core/ViewsAdapter.js +10 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.d.ts +15 -4
- package/dist/react/index.js +102 -15
- package/dist/react/reconciler.js +34 -12
- package/dist/vite/index.d.ts +9 -0
- package/dist/vite/index.js +36 -3
- package/package.json +33 -3
package/dist/core/SkiaControl.js
CHANGED
|
@@ -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
|
-
/**
|
|
25
|
-
|
|
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
|
-
//
|
|
29
|
-
|
|
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
|
-
/**
|
|
169
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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() {
|
|
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.
|
|
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
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
this.
|
|
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;
|