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.
- package/README.md +17 -4
- package/SKIPPED.md +31 -25
- 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 +2 -0
- package/dist/controls/SkiaBackdrop.js +2 -0
- 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 +202 -7
- package/dist/core/SkiaControl.js +467 -55
- package/dist/core/SkiaEffect.d.ts +7 -0
- package/dist/core/SkiaEffect.js +25 -6
- 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 +29 -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();
|
|
@@ -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
|
-
/**
|
|
169
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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() {
|
|
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.
|
|
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
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
this.
|
|
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;
|