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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +17 -4
  2. package/SKIPPED.md +31 -25
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +40 -0
  6. package/dist/controls/DrawnGame.js +64 -0
  7. package/dist/controls/GridStructure.d.ts +16 -2
  8. package/dist/controls/GridStructure.js +40 -4
  9. package/dist/controls/SkiaBackdrop.d.ts +2 -0
  10. package/dist/controls/SkiaBackdrop.js +2 -0
  11. package/dist/controls/SkiaButton.d.ts +19 -2
  12. package/dist/controls/SkiaButton.js +41 -11
  13. package/dist/controls/SkiaCarousel.d.ts +9 -1
  14. package/dist/controls/SkiaCarousel.js +16 -3
  15. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  16. package/dist/controls/SkiaCheckbox.js +0 -6
  17. package/dist/controls/SkiaDrawer.d.ts +2 -0
  18. package/dist/controls/SkiaDrawer.js +2 -0
  19. package/dist/controls/SkiaEditor.d.ts +12 -1
  20. package/dist/controls/SkiaEditor.js +46 -19
  21. package/dist/controls/SkiaGif.d.ts +3 -0
  22. package/dist/controls/SkiaGif.js +12 -2
  23. package/dist/controls/SkiaImage.js +15 -10
  24. package/dist/controls/SkiaLabel.d.ts +54 -3
  25. package/dist/controls/SkiaLabel.js +235 -52
  26. package/dist/controls/SkiaLabelFps.d.ts +16 -0
  27. package/dist/controls/SkiaLabelFps.js +41 -0
  28. package/dist/controls/SkiaLayout.d.ts +64 -7
  29. package/dist/controls/SkiaLayout.js +316 -19
  30. package/dist/controls/SkiaLottie.js +4 -2
  31. package/dist/controls/SkiaProgress.d.ts +2 -1
  32. package/dist/controls/SkiaProgress.js +6 -3
  33. package/dist/controls/SkiaRadioButton.js +4 -3
  34. package/dist/controls/SkiaScroll.d.ts +35 -5
  35. package/dist/controls/SkiaScroll.js +233 -23
  36. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  37. package/dist/controls/SkiaScrollBar.js +51 -2
  38. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  39. package/dist/controls/SkiaShaderCarousel.js +5 -0
  40. package/dist/controls/SkiaShape.d.ts +13 -2
  41. package/dist/controls/SkiaShape.js +50 -10
  42. package/dist/controls/SkiaSlider.d.ts +20 -2
  43. package/dist/controls/SkiaSlider.js +59 -5
  44. package/dist/controls/SkiaSprite.d.ts +1 -0
  45. package/dist/controls/SkiaSprite.js +4 -0
  46. package/dist/controls/SkiaSvg.js +1 -1
  47. package/dist/controls/SkiaSwitch.d.ts +0 -1
  48. package/dist/controls/SkiaSwitch.js +0 -6
  49. package/dist/controls/SkiaToggle.d.ts +25 -5
  50. package/dist/controls/SkiaToggle.js +58 -5
  51. package/dist/controls/SnappingLayout.d.ts +12 -1
  52. package/dist/controls/SnappingLayout.js +19 -1
  53. package/dist/core/Accessibility.d.ts +80 -0
  54. package/dist/core/Accessibility.js +365 -8
  55. package/dist/core/Animators.d.ts +37 -1
  56. package/dist/core/Animators.js +102 -9
  57. package/dist/core/Canvas.d.ts +108 -7
  58. package/dist/core/Canvas.js +363 -35
  59. package/dist/core/Easing.d.ts +12 -1
  60. package/dist/core/Easing.js +25 -1
  61. package/dist/core/Gestures.d.ts +48 -2
  62. package/dist/core/Gestures.js +95 -3
  63. package/dist/core/Hover.d.ts +47 -0
  64. package/dist/core/Hover.js +134 -0
  65. package/dist/core/KeyboardManager.d.ts +13 -0
  66. package/dist/core/KeyboardManager.js +18 -0
  67. package/dist/core/ScrollAnimators.js +9 -0
  68. package/dist/core/SkiaControl.d.ts +202 -7
  69. package/dist/core/SkiaControl.js +467 -55
  70. package/dist/core/SkiaEffect.d.ts +7 -0
  71. package/dist/core/SkiaEffect.js +25 -6
  72. package/dist/core/SkiaImageManager.js +1 -1
  73. package/dist/core/Styles.d.ts +51 -0
  74. package/dist/core/Styles.js +92 -0
  75. package/dist/core/Super.d.ts +17 -1
  76. package/dist/core/Super.js +35 -4
  77. package/dist/core/ViewsAdapter.d.ts +7 -2
  78. package/dist/core/ViewsAdapter.js +10 -3
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +4 -0
  81. package/dist/react/SkiaShell.js +33 -14
  82. package/dist/react/index.d.ts +15 -4
  83. package/dist/react/index.js +102 -15
  84. package/dist/react/reconciler.js +34 -12
  85. package/dist/vite/index.d.ts +9 -0
  86. package/dist/vite/index.js +36 -3
  87. package/package.json +29 -3
@@ -8,6 +8,8 @@ import { SkiaLayout } from "./SkiaLayout";
8
8
  * on every frame it is drawn in; inside a cached parent it is re-recorded with that parent).
9
9
  */
10
10
  export declare class SkiaBackdrop extends SkiaLayout {
11
+ /** Samples what is drawn below: a composite above redraws it whole when something inside changes (drawnui-cross 6m). */
12
+ protected get SamplesContent(): boolean;
11
13
  Blur: number;
12
14
  Brightness: number;
13
15
  /** Snapshot the surface currently drawn into (true) or the on-screen surface (false). */
@@ -8,6 +8,8 @@ import { SkiaLayout } from "./SkiaLayout";
8
8
  * on every frame it is drawn in; inside a cached parent it is re-recorded with that parent).
9
9
  */
10
10
  export class SkiaBackdrop extends SkiaLayout {
11
+ /** Samples what is drawn below: a composite above redraws it whole when something inside changes (drawnui-cross 6m). */
12
+ get SamplesContent() { return true; }
11
13
  Blur = 5;
12
14
  Brightness = 1;
13
15
  /** Snapshot the surface currently drawn into (true) or the on-screen surface (false). */
@@ -10,6 +10,8 @@ import { type PrebuiltControlStyle, type ResolvedControlStyle } from "../core/Co
10
10
  * PanThreshold. Press feedback = ApplyEffect (ripple), no darkening.
11
11
  */
12
12
  export declare class SkiaButton extends SkiaLayout {
13
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
14
+ protected ReceivesHoverByDefault(): boolean;
13
15
  static PanThreshold: number;
14
16
  Text: string;
15
17
  /** Look of the default content (C# SkiaButton.ControlStyle): background, corners, font, minimum size when left unset. */
@@ -24,7 +26,17 @@ export declare class SkiaButton extends SkiaLayout {
24
26
  StrokeColor: Color;
25
27
  StrokeWidth: number;
26
28
  IsPressed: boolean;
27
- IsDisabled: boolean;
29
+ private respondsToGestures;
30
+ /**
31
+ * False switches the button off for the user: taps, presses and keyboard activation do nothing (no Tapped, Down, Up),
32
+ * a tap on it does not reach controls under it, and screen readers report it disabled. Code can still change it.
33
+ * Same property as on toggles, sliders, scrolls and drawers (drawnui-cross 6w). Default true.
34
+ */
35
+ get RespondsToGestures(): boolean;
36
+ set RespondsToGestures(v: boolean);
37
+ /** @deprecated The opposite of RespondsToGestures, which every control uses: set `RespondsToGestures = false`. */
38
+ get IsDisabled(): boolean;
39
+ set IsDisabled(v: boolean);
28
40
  LockPanning: boolean;
29
41
  /** Touch feedback played on Down (DrawnUi SkiaButton.ApplyEffect). */
30
42
  ApplyEffect: SkiaTouchAnimation;
@@ -41,10 +53,15 @@ export declare class SkiaButton extends SkiaLayout {
41
53
  static DefaultAccessibilityRole?: string;
42
54
  protected DefaultAccessibilityLabel(): string | undefined;
43
55
  protected DefaultAccessibilityCanInteract(): boolean;
56
+ /** With RespondsToGestures = false the button ignores taps, keyboard navigation skips it and it is reported disabled. */
57
+ protected AcceptsInput(): boolean;
44
58
  /** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
45
59
  protected PaintBackground(): void;
46
60
  get UsingControlStyle(): ResolvedControlStyle;
47
- /** C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size. */
61
+ /**
62
+ * C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size.
63
+ * Every C# style calls SetDefaultMinimumContentSize(100, minH), so the width floor is 100 everywhere.
64
+ */
48
65
  private static Look;
49
66
  protected MeasureAbsolute(w: number, h: number, scale: number): ScaledSize;
50
67
  /** The frame sits under the padding box: arrange children in the full rect, the label centered inside padding. */
@@ -10,6 +10,8 @@ import { ResolveControlStyle } from "../core/ControlStyle";
10
10
  * PanThreshold. Press feedback = ApplyEffect (ripple), no darkening.
11
11
  */
12
12
  export class SkiaButton extends SkiaLayout {
13
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
14
+ ReceivesHoverByDefault() { return true; }
13
15
  static PanThreshold = 5;
14
16
  Text = "";
15
17
  /** Look of the default content (C# SkiaButton.ControlStyle): background, corners, font, minimum size when left unset. */
@@ -24,7 +26,20 @@ export class SkiaButton extends SkiaLayout {
24
26
  StrokeColor = Colors.Transparent;
25
27
  StrokeWidth = 0;
26
28
  IsPressed = false;
27
- IsDisabled = false;
29
+ respondsToGestures = true;
30
+ /**
31
+ * False switches the button off for the user: taps, presses and keyboard activation do nothing (no Tapped, Down, Up),
32
+ * a tap on it does not reach controls under it, and screen readers report it disabled. Code can still change it.
33
+ * Same property as on toggles, sliders, scrolls and drawers (drawnui-cross 6w). Default true.
34
+ */
35
+ get RespondsToGestures() { return this.respondsToGestures; }
36
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
37
+ this.respondsToGestures = v;
38
+ this.NotifyAccessibility();
39
+ } }
40
+ /** @deprecated The opposite of RespondsToGestures, which every control uses: set `RespondsToGestures = false`. */
41
+ get IsDisabled() { return !this.respondsToGestures; }
42
+ set IsDisabled(v) { this.RespondsToGestures = !v; }
28
43
  LockPanning = false;
29
44
  /** Touch feedback played on Down (DrawnUi SkiaButton.ApplyEffect). */
30
45
  ApplyEffect = "Ripple"; // C# default
@@ -49,22 +64,32 @@ export class SkiaButton extends SkiaLayout {
49
64
  this.label.VerticalOptions = "Center";
50
65
  this.AddSubView(this.frame);
51
66
  this.AddSubView(this.label);
67
+ // C# ButtonLabel is styled when the button builds its content, and ApplyProperties then overwrites FontFamily with
68
+ // the button's own (empty by default) value, which the style no longer touches. Same order here: style the caption
69
+ // now, MeasureAbsolute below pushes SkiaButton.FontFamily over it every measure.
70
+ this.frame.ApplyInitialStyles(true);
71
+ this.label.ApplyInitialStyles(true);
52
72
  }
53
73
  /** Set to `Aria.RoleButton` to expose every button (React extension). */
54
74
  static DefaultAccessibilityRole;
55
75
  DefaultAccessibilityLabel() { return this.Text || undefined; }
56
- DefaultAccessibilityCanInteract() { return !this.IsDisabled; }
76
+ DefaultAccessibilityCanInteract() { return this.respondsToGestures; }
77
+ /** With RespondsToGestures = false the button ignores taps, keyboard navigation skips it and it is reported disabled. */
78
+ AcceptsInput() { return this.respondsToGestures; }
57
79
  /** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
58
80
  PaintBackground() { }
59
81
  get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
60
- /** C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size. */
82
+ /**
83
+ * C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size.
84
+ * Every C# style calls SetDefaultMinimumContentSize(100, minH), so the width floor is 100 everywhere.
85
+ */
61
86
  static Look(s) {
62
87
  switch (s) {
63
- case "Cupertino": return { bg: "#007AFF", radius: 8, font: 17, weight: 600, minH: 36, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.2, Color: Colors.Black }) };
64
- case "Material": return { bg: "#2196F3", radius: 4, font: 14, weight: 0, minH: 40, shadow: new SkiaShadow({ X: 0, Y: 2, Blur: 4, Opacity: 0.3, Color: Colors.Black }) };
65
- case "Material3": return { bg: "#6750A4", radius: 20, font: 14, weight: 0, minH: 40 };
66
- case "Windows": return { bg: "#0078D7", radius: 4, font: 15, weight: 500, minH: 32, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 1, Opacity: 0.2, Color: Colors.Black }) };
67
- default: return { bg: "#DC143C", radius: 8, font: 15, weight: 0, minH: 41 };
88
+ case "Cupertino": return { bg: "#007AFF", radius: 8, font: 17, weight: 600, minW: 100, minH: 36, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.2, Color: Colors.Black }) };
89
+ case "Material": return { bg: "#2196F3", radius: 4, font: 14, weight: 0, minW: 100, minH: 40, shadow: new SkiaShadow({ X: 0, Y: 2, Blur: 4, Opacity: 0.3, Color: Colors.Black }) };
90
+ case "Material3": return { bg: "#6750A4", radius: 20, font: 14, weight: 0, minW: 100, minH: 40 };
91
+ case "Windows": return { bg: "#0078D7", radius: 4, font: 15, weight: 500, minW: 100, minH: 32, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 1, Opacity: 0.2, Color: Colors.Black }) };
92
+ default: return { bg: "#DC143C", radius: 8, font: 15, weight: 0, minW: 100, minH: 41 };
68
93
  }
69
94
  }
70
95
  MeasureAbsolute(w, h, scale) {
@@ -75,7 +100,12 @@ export class SkiaButton extends SkiaLayout {
75
100
  this.frame.StrokeWidth = this.StrokeWidth;
76
101
  if (this.frame.Shadows[0] !== look.shadow)
77
102
  this.frame.Shadows = look.shadow ? [look.shadow] : [];
78
- if (this.MinimumHeightRequest < 0 && this.UsingControlStyle !== "Unset")
103
+ // C# SetDefaultMinimumContentSize(100, minH) from the style builder: a style default only where the app left the
104
+ // size unset, skipped on a Fill axis. C# adds Margins because it compares the minimum against a margin-inclusive
105
+ // size; here Measure applies it to the margin-excluded size and adds the margin after, so the plain value is used.
106
+ if (this.WidthRequest < 0 && this.MinimumWidthRequest < 0 && this.HorizontalOptions !== "Fill")
107
+ this.MinimumWidthRequest = look.minW;
108
+ if (this.HeightRequest < 0 && this.MinimumHeightRequest < 0 && this.VerticalOptions !== "Fill")
79
109
  this.MinimumHeightRequest = look.minH;
80
110
  this.label.Text = this.Text;
81
111
  this.label.TextColor = this.TextColor;
@@ -113,8 +143,8 @@ export class SkiaButton extends SkiaLayout {
113
143
  this.Repaint();
114
144
  }
115
145
  ProcessGestures(args, apply) {
116
- if (this.IsDisabled)
117
- return null;
146
+ if (!this.respondsToGestures)
147
+ return this.BlockedGestureResult(args); // no press, no Down / Up / Tapped; a tap on it stops here
118
148
  const point = args.Event.Location;
119
149
  if (args.Type === "Down") {
120
150
  this.IsPressed = true;
@@ -15,6 +15,8 @@ export interface SnapPoint {
15
15
  * `DynamicSize` sizes an auto-sized carousel from the selected slide, `SwipeSpeed` / `LinearSpeedMs` tune the snap.
16
16
  */
17
17
  export declare class SkiaCarousel extends SnappingLayout {
18
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
19
+ protected ReceivesHoverByDefault(): boolean;
18
20
  IsVertical: boolean;
19
21
  /** Side padding in points so the previous/next slides peek in. */
20
22
  SidesOffset: number;
@@ -99,7 +101,7 @@ export declare class SkiaCarousel extends SnappingLayout {
99
101
  private GetVirtualSnapPoints;
100
102
  protected GetVirtualAnchor(current: SKPoint): SnapPoint;
101
103
  protected FindNearestAnchorInternal(current: SKPoint, velocity: SKPoint): SKPoint;
102
- /** C# SkiaCarousel.ScrollToNearestAnchor: velocity below 100 counts as none; looped snaps around virtual anchors. */
104
+ /** C# SkiaCarousel.ScrollToNearestAnchor: velocity below SnapVelocityThreshold counts as none; looped snaps around virtual anchors. */
103
105
  ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
104
106
  /** C# SkiaCarousel.SelectNextAnchor: base choice, plus a wrap to the virtual anchor at the strip borders when looped. */
105
107
  SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
@@ -127,6 +129,12 @@ export declare class SkiaCarousel extends SnappingLayout {
127
129
  /** C# RenderViewsList: position every slide, draw the visible ones (+ neighbours when PreloadNeighboors), release the rest. */
128
130
  protected Paint(ctx: DrawingContext): void;
129
131
  private SlideRect;
132
+ /**
133
+ * Whether the slide at `index` takes part in hit-testing for the frame being rendered (C# IsSlideHitTestable).
134
+ * Every slide drawn on screen does by default; a carousel whose slides share one rect (SkiaShaderCarousel) narrows
135
+ * this to the slide the user actually sees.
136
+ */
137
+ protected IsSlideHitTestable(_index: number): boolean;
130
138
  protected GetGestureListeners(): readonly SkiaControl[];
131
139
  ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
132
140
  protected DefaultAccessibilityLabel(): string | undefined;
@@ -11,6 +11,8 @@ import { SnappingLayout } from "./SnappingLayout";
11
11
  * `DynamicSize` sizes an auto-sized carousel from the selected slide, `SwipeSpeed` / `LinearSpeedMs` tune the snap.
12
12
  */
13
13
  export class SkiaCarousel extends SnappingLayout {
14
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
15
+ ReceivesHoverByDefault() { return true; }
14
16
  IsVertical = false;
15
17
  /** Side padding in points so the previous/next slides peek in. */
16
18
  SidesOffset = 0;
@@ -230,9 +232,10 @@ export class SkiaCarousel extends SnappingLayout {
230
232
  }
231
233
  return super.FindNearestAnchorInternal(current, velocity);
232
234
  }
233
- /** C# SkiaCarousel.ScrollToNearestAnchor: velocity below 100 counts as none; looped snaps around virtual anchors. */
235
+ /** C# SkiaCarousel.ScrollToNearestAnchor: velocity below SnapVelocityThreshold counts as none; looped snaps around virtual anchors. */
234
236
  ScrollToNearestAnchor(location, velocity) {
235
- velocity = new SKPoint(Math.abs(velocity.X) < 100 ? 0 : velocity.X, Math.abs(velocity.Y) < 100 ? 0 : velocity.Y);
237
+ const t = this.SnapVelocityThreshold;
238
+ velocity = new SKPoint(Math.abs(velocity.X) < t ? 0 : velocity.X, Math.abs(velocity.Y) < t ? 0 : velocity.Y);
236
239
  if (this.ApplyLoopedLogic) {
237
240
  const origin = this.FindNearestAnchorInternal(location, velocity);
238
241
  const target = this.SelectNextAnchor(origin, velocity);
@@ -504,7 +507,7 @@ export class SkiaCarousel extends SnappingLayout {
504
507
  view.Measure(inner.Width, inner.Height, scale); // recycled cells carry new content
505
508
  view.Arrange(this.SlideRect(inner, this.SlideOffset(cell.offset), scale), view.WidthRequest, view.HeightRequest, scale);
506
509
  view.Render(ctx);
507
- if (cell.visible)
510
+ if (cell.visible && this.IsSlideHitTestable(cell.index))
508
511
  visible.push(view);
509
512
  }
510
513
  this.visibleViews = visible;
@@ -514,6 +517,12 @@ export class SkiaCarousel extends SnappingLayout {
514
517
  ? SKRect.Create(inner.Left, inner.Top + offset.Y * scale, inner.Width, inner.Height)
515
518
  : SKRect.Create(inner.Left + offset.X * scale, inner.Top, inner.Width, inner.Height);
516
519
  }
520
+ /**
521
+ * Whether the slide at `index` takes part in hit-testing for the frame being rendered (C# IsSlideHitTestable).
522
+ * Every slide drawn on screen does by default; a carousel whose slides share one rect (SkiaShaderCarousel) narrows
523
+ * this to the slide the user actually sees.
524
+ */
525
+ IsSlideHitTestable(_index) { return true; }
517
526
  GetGestureListeners() { return this.visibleViews; }
518
527
  // ---- gestures (port of C# SkiaCarousel.ProcessGestures) ----
519
528
  ProcessGestures(args, apply) {
@@ -555,6 +564,10 @@ export class SkiaCarousel extends SnappingLayout {
555
564
  if (!this.IsUserPanning) {
556
565
  const movex = Math.abs(e.Distance.Total.X), movey = Math.abs(e.Distance.Total.Y);
557
566
  const along = this.IsVertical ? movey : movex, across = this.IsVertical ? movex : movey;
567
+ // a first move too short to tell the direction decides nothing yet (a 1 CSS px move at devicePixelRatio 2):
568
+ // marking it wrong kept the whole drag dead until the next press (DrawnUi.Rust c6b22e3; C# decides on every move)
569
+ if (along < scale * 2 && across < scale * 2)
570
+ return consumedDefault;
558
571
  if (along < scale * 2 || across > along) {
559
572
  this.wrongDirection = true;
560
573
  return consumedDefault;
@@ -15,7 +15,6 @@ export declare class SkiaCheckbox extends SkiaToggle {
15
15
  get ColorCheckOn(): Color;
16
16
  set ColorCheckOn(v: Color);
17
17
  protected StyleDefault(name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
18
- private SetContentSize;
19
18
  protected CreateDefaultContent(): void;
20
19
  ApplyProperties(): void;
21
20
  }
@@ -27,12 +27,6 @@ export class SkiaCheckbox extends SkiaToggle {
27
27
  };
28
28
  return table[this.UsingControlStyle]?.[name];
29
29
  }
30
- SetContentSize(w, h) {
31
- if (this.WidthRequest < 0)
32
- this.WidthRequest = w;
33
- if (this.HeightRequest < 0)
34
- this.HeightRequest = h;
35
- }
36
30
  CreateDefaultContent() {
37
31
  const off = new SkiaShape();
38
32
  off.Tag = "FrameOff";
@@ -9,6 +9,8 @@ import { SnappingLayout } from "./SnappingLayout";
9
9
  * so it must be aligned to its edge by the parent (VerticalOptions="End" for FromBottom, etc.).
10
10
  */
11
11
  export declare class SkiaDrawer extends SnappingLayout {
12
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
13
+ protected ReceivesHoverByDefault(): boolean;
12
14
  Direction: DrawerDirection;
13
15
  /** Points that stay visible when closed. */
14
16
  HeaderSize: number;
@@ -7,6 +7,8 @@ import { SnappingLayout } from "./SnappingLayout";
7
7
  * so it must be aligned to its edge by the parent (VerticalOptions="End" for FromBottom, etc.).
8
8
  */
9
9
  export class SkiaDrawer extends SnappingLayout {
10
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
11
+ ReceivesHoverByDefault() { return true; }
10
12
  Direction = "FromBottom";
11
13
  /** Points that stay visible when closed. */
12
14
  HeaderSize = 0;
@@ -58,7 +58,8 @@ export declare class SkiaEditor extends SkiaShape {
58
58
  private readonly placeholder;
59
59
  private scrollX;
60
60
  private scrollY;
61
- private blink;
61
+ /** The caret blinks on a timer, asleep between blinks: no frame runs while it waits (DrawnUi.Rust editor_rules). */
62
+ private blinkTimer;
62
63
  private caretVisible;
63
64
  private subscribed;
64
65
  private stubSelectionStop;
@@ -102,6 +103,9 @@ export declare class SkiaEditor extends SkiaShape {
102
103
  set CursorPosition(v: number);
103
104
  get SelectionLength(): number;
104
105
  set SelectionLength(v: number);
106
+ /** Shows the caret for a full half second, then blinks every 500 ms. */
107
+ private StartBlink;
108
+ private StopBlink;
105
109
  private OnCursorMoved;
106
110
  get IsFocused(): boolean;
107
111
  set IsFocused(v: boolean);
@@ -128,7 +132,14 @@ export declare class SkiaEditor extends SkiaShape {
128
132
  private LabelPoint;
129
133
  protected Paint(ctx: DrawingContext): void;
130
134
  ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
135
+ /** A text field is a Tab stop by default, like a native text box, with no Tapped handler needed. */
136
+ protected DefaultAccessibilityCanInteract(): boolean;
137
+ /** Keyboard navigation reached the field (Tab) or left it: take the caret and the input sink like a native text box, or release them. */
138
+ OnAccessibilityFocused(focused: boolean): void;
139
+ /** Enter / Space on the field while it has no caret start editing; while editing they are text. */
131
140
  OnAccessibilityActivated(): void;
141
+ /** While editing, arrows, Home / End and PageUp / PageDown belong to the text: they never move a surrounding group. */
142
+ OnAccessibilityKey(_key: string): boolean;
132
143
  protected DefaultAccessibilityLabel(): string | undefined;
133
144
  Submit(): void;
134
145
  private ExecuteSubmit;
@@ -1,6 +1,5 @@
1
1
  import { SKPoint } from "../core/Gestures";
2
2
  import { Super } from "../core/Super";
3
- import { SkiaValueAnimator } from "../core/Animators";
4
3
  import { ResolveControlStyle } from "../core/ControlStyle";
5
4
  import { KeyboardManager } from "../core/KeyboardManager";
6
5
  import { TextInputProxy } from "../core/TextInputProxy";
@@ -67,13 +66,19 @@ export class SkiaEditor extends SkiaShape {
67
66
  placeholder = new SkiaLabel();
68
67
  scrollX = 0;
69
68
  scrollY = 0;
70
- blink = new SkiaValueAnimator(this);
69
+ /** The caret blinks on a timer, asleep between blinks: no frame runs while it waits (DrawnUi.Rust editor_rules). */
70
+ blinkTimer = 0;
71
71
  caretVisible = true;
72
72
  subscribed = false;
73
73
  stubSelectionStop = -1;
74
74
  selectionMovingEdge = -1;
75
75
  onKeyDown = (key, e) => this.OnKeyDown(key, e);
76
- onKeyChar = (ch, e) => { e.preventDefault(); this.StubTypeText(ch); };
76
+ onKeyChar = (ch, e) => {
77
+ if (KeyboardManager.IsOwnedByElement(e) || KeyboardManager.IsInputMethodKey(e))
78
+ return; // a page control or the IME owns it
79
+ e.preventDefault();
80
+ this.StubTypeText(ch);
81
+ };
77
82
  selectionAnchor = -1;
78
83
  dragSelecting = false;
79
84
  styleApplied = {};
@@ -93,15 +98,6 @@ export class SkiaEditor extends SkiaShape {
93
98
  this.placeholder.IsVisible = false;
94
99
  this.AddSubView(this.placeholder);
95
100
  this.AddSubView(this.Label);
96
- this.blink.mMinValue = 0;
97
- this.blink.mMaxValue = 1;
98
- this.blink.Speed = 1000;
99
- this.blink.Repeat = -1;
100
- this.blink.OnUpdated = (v) => { const visible = v < 0.5; if (visible !== this.caretVisible) {
101
- this.caretVisible = visible;
102
- this.InvalidateCache();
103
- this.RepaintComposition();
104
- } };
105
101
  this.ApplyControlStyleVisuals();
106
102
  this.UpdateLabel();
107
103
  }
@@ -192,9 +188,22 @@ export class SkiaEditor extends SkiaShape {
192
188
  this.selectionLength = l;
193
189
  this.OnCursorMoved();
194
190
  }
191
+ /** Shows the caret for a full half second, then blinks every 500 ms. */
192
+ StartBlink() {
193
+ this.StopBlink();
194
+ this.caretVisible = true;
195
+ this.blinkTimer = window.setInterval(() => { this.caretVisible = !this.caretVisible; this.InvalidateCache(); this.RepaintComposition(); }, 500);
196
+ }
197
+ StopBlink() { if (this.blinkTimer) {
198
+ clearInterval(this.blinkTimer);
199
+ this.blinkTimer = 0;
200
+ } }
195
201
  OnCursorMoved() {
196
202
  this.ScrollToCaret();
197
- this.caretVisible = true;
203
+ if (this.isFocused)
204
+ this.StartBlink();
205
+ else
206
+ this.caretVisible = true;
198
207
  TextInputProxy.Sync(this);
199
208
  this.CursorMoved?.(this);
200
209
  this.InvalidateCache();
@@ -210,8 +219,7 @@ export class SkiaEditor extends SkiaShape {
210
219
  SkiaEditor.Focused.IsFocused = false;
211
220
  SkiaEditor.Focused = this;
212
221
  this.SetFocusNative(true);
213
- this.caretVisible = true;
214
- this.blink.Start();
222
+ this.StartBlink();
215
223
  }
216
224
  else {
217
225
  if (SkiaEditor.Focused === this)
@@ -220,7 +228,7 @@ export class SkiaEditor extends SkiaShape {
220
228
  this.dragSelecting = false;
221
229
  this.stubSelectionStop = -1;
222
230
  this.selectionMovingEdge = -1;
223
- this.blink.Stop();
231
+ this.StopBlink();
224
232
  this.SetFocusNative(false);
225
233
  }
226
234
  this.FocusChanged?.(this, v);
@@ -305,6 +313,14 @@ export class SkiaEditor extends SkiaShape {
305
313
  MeasureAbsolute(w, h, scale) {
306
314
  const px = this.Padding.HorizontalThickness * scale, py = this.Padding.VerticalThickness * scale;
307
315
  const innerW = isFinite(w) ? Math.max(0, w - px) : Infinity;
316
+ // Single-line: the label is Start-aligned and measures to its content, so Center / End alignment had nothing to
317
+ // align in. Give it the field as a MINIMUM: short text centers in the field, longer text still grows past it and
318
+ // scrolls (C# 2e334ddd).
319
+ const minWidth = !this.IsMultiline && this.horizontalTextAlignment !== "Start" && isFinite(innerW) && innerW > 0 ? innerW / scale : -1;
320
+ if (this.Label.MinimumWidthRequest !== minWidth) {
321
+ this.Label.MinimumWidthRequest = minWidth;
322
+ this.Label.NeedMeasure = true;
323
+ }
308
324
  const labelSize = this.Label.Measure(this.IsMultiline ? innerW : Infinity, Infinity, scale);
309
325
  if (this.placeholder.IsVisible)
310
326
  this.placeholder.Measure(innerW, Infinity, scale);
@@ -500,7 +516,15 @@ export class SkiaEditor extends SkiaShape {
500
516
  }
501
517
  return this;
502
518
  }
503
- OnAccessibilityActivated() { this.IsFocused = true; }
519
+ /** A text field is a Tab stop by default, like a native text box, with no Tapped handler needed. */
520
+ DefaultAccessibilityCanInteract() { return true; }
521
+ /** Keyboard navigation reached the field (Tab) or left it: take the caret and the input sink like a native text box, or release them. */
522
+ OnAccessibilityFocused(focused) { this.IsFocused = focused; }
523
+ /** Enter / Space on the field while it has no caret start editing; while editing they are text. */
524
+ OnAccessibilityActivated() { if (!this.IsFocused)
525
+ this.OnAccessibilityFocused(true); }
526
+ /** While editing, arrows, Home / End and PageUp / PageDown belong to the text: they never move a surrounding group. */
527
+ OnAccessibilityKey(_key) { return this.IsFocused; }
504
528
  DefaultAccessibilityLabel() { return this.text || this.placeholderText || undefined; }
505
529
  // ---- editing API (C# names) ----
506
530
  Submit() {
@@ -580,6 +604,10 @@ export class SkiaEditor extends SkiaShape {
580
604
  OnKeyDown(key, e) {
581
605
  const shift = KeyboardManager.IsShiftPressed, ctrl = KeyboardManager.IsControlPressed, alt = KeyboardManager.IsAltPressed;
582
606
  const handled = () => e.preventDefault();
607
+ // keys of another element (a page field or button, an overlay node) and of the input method are not the editor's:
608
+ // a composition keeps its arrows, Escape and Backspace (C# 845b26e9, GitHub #231)
609
+ if (KeyboardManager.IsInputMethodKey(e) || (KeyboardManager.IsOwnedByElement(e) && !TextInputProxy.IsProxyTarget(e.target)))
610
+ return;
583
611
  // with the hidden textarea focused these keys become input / clipboard events there and are applied from those
584
612
  if (TextInputProxy.IsActive(this) && TextInputProxy.IsProxyTarget(e.target) && (key === "Backspace" || key === "Delete" || key === "Enter" || key === "NumpadEnter" || (ctrl && (key === "KeyC" || key === "KeyX" || key === "KeyV"))))
585
613
  return;
@@ -794,8 +822,7 @@ export class SkiaEditor extends SkiaShape {
794
822
  return out;
795
823
  }
796
824
  OnDisposing() {
797
- this.blink.Stop();
798
- this.blink.Dispose();
825
+ this.StopBlink();
799
826
  if (SkiaEditor.Focused === this)
800
827
  SkiaEditor.Focused = undefined;
801
828
  this.SetFocusNative(false);
@@ -14,6 +14,8 @@ export declare class GifAnimation {
14
14
  private positionsMs;
15
15
  /** Negative = the last frame. */
16
16
  SeekFrame(frame: number): void;
17
+ /** Start (ms) of the frame after the one at `msTime`; DurationMs after the last one. */
18
+ NextFrameStartMs(msTime: number): number;
17
19
  GetFrameNumber(msTime: number): number;
18
20
  /** Decodes every frame of an encoded GIF (CanvasKit AnimatedImage). */
19
21
  LoadFromBytes(bytes: ArrayBuffer): void;
@@ -37,6 +39,7 @@ export declare class SkiaGif extends AnimatedFramesRenderer {
37
39
  set Source(v: string);
38
40
  Start(delayMs?: number): void;
39
41
  protected OnAnimatorUpdated(value: number): void;
42
+ protected NextChangeMs(value: number): number | undefined;
40
43
  /** Seeks by time in ms (the animator range is 0..DurationMs). */
41
44
  protected OnAnimatorSeeking(time: number): void;
42
45
  protected OnAnimatorInitializing(): void;
@@ -21,6 +21,13 @@ export class GifAnimation {
21
21
  if (frame >= 0 && frame <= this.TotalFrames - 1)
22
22
  this.Frame = this.Frames[frame];
23
23
  }
24
+ /** Start (ms) of the frame after the one at `msTime`; DurationMs after the last one. */
25
+ NextFrameStartMs(msTime) {
26
+ for (let i = 0; i < this.positionsMs.length; i++)
27
+ if (msTime < this.positionsMs[i])
28
+ return this.positionsMs[i];
29
+ return this.DurationMs;
30
+ }
24
31
  GetFrameNumber(msTime) {
25
32
  if (this.positionsMs.length === 0 || this.DurationMs <= 0)
26
33
  return 0;
@@ -87,7 +94,9 @@ export class SkiaGif extends AnimatedFramesRenderer {
87
94
  loadGeneration = 0;
88
95
  constructor() {
89
96
  super();
90
- this.UseCache = "ImageDoubleBuffered";
97
+ // C# ImageDoubleBuffered pays off where a thread bakes the bitmap; on the web it is one offscreen pass per frame
98
+ // change, so the web engines draw a GIF frame with no cache (drawnui-cross rule 3)
99
+ this.UseCache = "None";
91
100
  }
92
101
  get Source() { return this.source; }
93
102
  set Source(v) { if (this.source !== v) {
@@ -97,6 +106,7 @@ export class SkiaGif extends AnimatedFramesRenderer {
97
106
  Start(delayMs = 0) { if ((this.Animation?.TotalFrames ?? 0) > 0)
98
107
  super.Start(delayMs); }
99
108
  OnAnimatorUpdated(value) { this.Seek(value); }
109
+ NextChangeMs(value) { return this.Animation && this.Animation.DurationMs > 0 ? this.Animation.NextFrameStartMs(value) : undefined; }
100
110
  /** Seeks by time in ms (the animator range is 0..DurationMs). */
101
111
  OnAnimatorSeeking(time) {
102
112
  const a = this.Animation;
@@ -158,7 +168,7 @@ export class SkiaGif extends AnimatedFramesRenderer {
158
168
  async LoadSource(source) {
159
169
  if (!source)
160
170
  return undefined;
161
- const r = await fetch(source);
171
+ const r = await fetch(Super.ResolveAssetUrl(source));
162
172
  if (!r.ok)
163
173
  throw new Error(`${r.status} ${source}`);
164
174
  const animation = new GifAnimation();
@@ -164,20 +164,25 @@ export class SkiaImage extends SkiaControl {
164
164
  MeasureAbsolute(widthConstraint, heightConstraint, scale) {
165
165
  const img = this.LoadedSource;
166
166
  let w = widthConstraint, h = heightConstraint;
167
- if (img) {
168
- const aspect = img.width() / img.height();
169
- if (!isFinite(w) && isFinite(h))
170
- w = h * aspect;
171
- else if (!isFinite(h) && isFinite(w))
172
- h = w / aspect;
167
+ // nothing loaded: the box it is offered (C# OnMeasuring), an unbounded side 0
168
+ if (!img) {
169
+ this.AspectScale = { X: 0, Y: 0 };
170
+ return ScaledSize.FromPixels(isFinite(w) ? w : 0, isFinite(h) ? h : 0, scale);
173
171
  }
174
- if (!isFinite(w) || !isFinite(h)) {
172
+ const iw = img.width(), ih = img.height();
173
+ if (!isFinite(w) && isFinite(h))
174
+ w = h * iw / ih;
175
+ else if (!isFinite(h) && isFinite(w))
176
+ h = w * ih / iw;
177
+ if (!isFinite(w) || !isFinite(h) || iw <= 0 || ih <= 0) {
175
178
  this.AspectScale = { X: 0, Y: 0 };
176
179
  return ScaledSize.FromPixels(0, 0, scale);
177
180
  }
178
- if (img)
179
- this.AspectScale = SkiaImage.RescaleAspect(img.width(), img.height(), SKRect.Create(0, 0, w, h), this.Aspect);
180
- return ScaledSize.FromPixels(w, h, scale);
181
+ // C# OnMeasuring / DrawnUi.Rust: an auto-sized side is the bitmap's pixels scaled by the aspect for this box, never
182
+ // more than the box (a Fill or requested side still takes its box in Measure)
183
+ const s = SkiaImage.RescaleAspect(iw, ih, SKRect.Create(0, 0, w, h), this.Aspect);
184
+ this.AspectScale = s;
185
+ return ScaledSize.FromPixels(Math.min(iw * s.X, w), Math.min(ih * s.Y, h), scale);
181
186
  }
182
187
  Paint(ctx) {
183
188
  const img = this.LoadedSource;