drawnui 0.1.0-preview.5

DrawnUI for Rust: a Skia-drawn UI engine, the same controls and contract as DrawnUI for .NET and React, on the desktop, mobile and the web.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
//! Value types with the DrawnUI names. Units are points unless a name says pixels.

use skia_safe::{BlendMode, Color, Point, TileMode};

/// Where a control sits inside the box its parent gives it.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum LayoutOptions {
    #[default]
    Start,
    Center,
    End,
    Fill,
}

#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub struct Thickness {
    pub left: f32,
    pub top: f32,
    pub right: f32,
    pub bottom: f32,
}

impl Thickness {
    pub const ZERO: Thickness = Thickness { left: 0.0, top: 0.0, right: 0.0, bottom: 0.0 };

    pub const fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
        Self { left, top, right, bottom }
    }
    pub const fn uniform(all: f32) -> Self {
        Self::new(all, all, all, all)
    }
    pub fn horizontal(&self) -> f32 {
        self.left + self.right
    }
    pub fn vertical(&self) -> f32 {
        self.top + self.bottom
    }
    /// The larger of the two on every side.
    pub fn max(self, other: Thickness) -> Thickness {
        let (a, b) = (self, other);
        Thickness::new(a.left.max(b.left), a.top.max(b.top), a.right.max(b.right), a.bottom.max(b.bottom))
    }
}

/// Where the canvas draws (DrawnUI `Canvas.RenderingMode`; Accelerated by default as in
/// DrawnUi.React, C# defaults to Default).
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum RenderingModeType {
    /// The CPU: each frame is drawn into memory, then shown. Caches are CPU bitmaps. A browser
    /// that refuses WebGL2 draws this way whatever was asked.
    Default,
    /// The GPU: WebGL2, OpenGL or Metal.
    #[default]
    Accelerated,
}

/// What the canvas does with gestures the page could take too (DrawnUI `Canvas.Gestures`). The
/// browser host acts on it; the native hosts own their window and ignore it.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum GesturesMode {
    /// The canvas takes its gestures; the page keeps what the app does not use (the wheel) and its
    /// own CSS decides touch panning.
    #[default]
    Enabled,
    /// The canvas owns every touch: no page scroll, bounce, pull-down or text selection starts on
    /// it (DrawnUi.Web `applyGestureStyle` with lock). For full-screen apps and games.
    Lock,
}

/// The GPU API the canvas draws with (DrawnUi.Rust; DrawnUI picks per platform itself).
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum GpuBackend {
    /// Vulkan on Android, OpenGL ES where Vulkan cannot be made; OpenGL on Windows and Linux,
    /// Metal on Apple platforms, WebGL2 in the browser.
    #[default]
    Auto,
    /// OpenGL (ES on Android) where the platform has a choice.
    OpenGl,
    /// Vulkan where the platform has it (Android), else as `Auto`.
    Vulkan,
}

/// What a control keeps between frames instead of painting again (DrawnUI SkiaCacheType).
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[allow(clippy::upper_case_acronyms)]
pub enum CacheType {
    #[default]
    None,
    /// Recorded draw commands, replayed each frame.
    Operations,
    /// Recorded draw commands over the whole area the canvas shows (its clip), not only the
    /// control's rect: for a control that paints outside its rect. Recorded again when the size
    /// of that area changes; a cache that must move inside a scroll belongs on the scrolled parent.
    OperationsFull,
    /// Offscreen image on the window's GPU context, blitted each frame (DrawnUI's GPU cache; its
    /// Image cache is a CPU bitmap).
    Image,
    /// DrawnUI GPU: the same as `Image`, which is on the GPU already.
    GPU,
    /// A CPU bitmap made off the frame thread: the last one is drawn while the next one is made
    /// (the desktop; the browser makes it in the frame, as `Image`).
    ImageDoubleBuffered,
    /// An Image cache whose surface is kept: when only some children changed, they and the
    /// siblings they overlap are drawn again into it, the rest stays.
    ImageComposite,
    /// DrawnUI ImageCompositeGPU: the same as `ImageComposite`, which is on the GPU already.
    ImageCompositeGPU,
}

impl CacheType {
    /// The cache a control gets: the GPU names are the caches that are on the GPU already.
    pub(crate) fn resolved(self) -> Self {
        match self {
            CacheType::GPU => CacheType::Image,
            CacheType::ImageCompositeGPU => CacheType::ImageComposite,
            other => other,
        }
    }
}

/// Which gestures a control lets through to its children.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum LockTouch {
    #[default]
    Disabled,
    Enabled,
    PassNone,
    PassTap,
    PassTapAndLongPress,
}

/// What a property change invalidates.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Dirty(pub(crate) u8);

impl Dirty {
    pub const NONE: Dirty = Dirty(0);
    /// Size may change: the control and its ancestors measure again, caches are dropped.
    pub const MEASURE: Dirty = Dirty(1);
    /// Own pixels change: own cache and ancestor caches are recorded again.
    pub const DRAW: Dirty = Dirty(2);
    /// Position, transform or opacity: own cache stays, ancestors composite again.
    pub const REPAINT: Dirty = Dirty(4);
    /// The control's `on_props_changed` runs before the next layout.
    pub const APPLY: Dirty = Dirty(8);
    pub const MEASURE_APPLY: Dirty = Dirty(1 | 8);
    pub const DRAW_APPLY: Dirty = Dirty(2 | 8);

    pub fn contains(self, other: Dirty) -> bool {
        self.0 & other.0 == other.0 && other.0 != 0
    }
    pub fn is_empty(self) -> bool {
        self.0 == 0
    }
}

impl std::ops::BitOrAssign for Dirty {
    fn bitor_assign(&mut self, rhs: Dirty) {
        self.0 |= rhs.0
    }
}

/// Conversion accepted by property builders and setters, so `16`, `16.0` and `"text"` all work.
pub trait IntoProp<T> {
    fn into_prop(self) -> T;
}

impl<T> IntoProp<T> for T {
    fn into_prop(self) -> T {
        self
    }
}
impl IntoProp<f32> for i32 {
    fn into_prop(self) -> f32 {
        self as f32
    }
}
impl IntoProp<f32> for f64 {
    fn into_prop(self) -> f32 {
        self as f32
    }
}
impl IntoProp<String> for &str {
    fn into_prop(self) -> String {
        self.to_owned()
    }
}
impl IntoProp<Option<Color>> for Color {
    fn into_prop(self) -> Option<Color> {
        Some(self)
    }
}
impl IntoProp<Option<char>> for char {
    fn into_prop(self) -> Option<char> {
        Some(self)
    }
}
impl IntoProp<Option<bool>> for bool {
    fn into_prop(self) -> Option<bool> {
        Some(self)
    }
}
impl IntoProp<Thickness> for f32 {
    fn into_prop(self) -> Thickness {
        Thickness::uniform(self)
    }
}
impl IntoProp<Thickness> for i32 {
    fn into_prop(self) -> Thickness {
        Thickness::uniform(self as f32)
    }
}
impl IntoProp<Thickness> for (f32, f32) {
    /// (horizontal, vertical)
    fn into_prop(self) -> Thickness {
        Thickness::new(self.0, self.1, self.0, self.1)
    }
}
impl IntoProp<Thickness> for (i32, i32) {
    fn into_prop(self) -> Thickness {
        (self.0 as f32, self.1 as f32).into_prop()
    }
}
impl IntoProp<Thickness> for (f32, f32, f32, f32) {
    /// (left, top, right, bottom)
    fn into_prop(self) -> Thickness {
        Thickness::new(self.0, self.1, self.2, self.3)
    }
}
impl IntoProp<Thickness> for (i32, i32, i32, i32) {
    fn into_prop(self) -> Thickness {
        Thickness::new(self.0 as f32, self.1 as f32, self.2 as f32, self.3 as f32)
    }
}

// ---------------------------------------------------------------- shapes, gradients, shadows

/// Corner radii in points, in the order of the MAUI constructor.
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub struct CornerRadius {
    pub top_left: f32,
    pub top_right: f32,
    pub bottom_left: f32,
    pub bottom_right: f32,
}

impl CornerRadius {
    pub const fn new(top_left: f32, top_right: f32, bottom_left: f32, bottom_right: f32) -> Self {
        Self { top_left, top_right, bottom_left, bottom_right }
    }
    pub const fn uniform(all: f32) -> Self {
        Self::new(all, all, all, all)
    }
    pub fn is_zero(&self) -> bool {
        *self == Self::default()
    }
}

impl IntoProp<CornerRadius> for f32 {
    fn into_prop(self) -> CornerRadius {
        CornerRadius::uniform(self)
    }
}
impl IntoProp<CornerRadius> for i32 {
    fn into_prop(self) -> CornerRadius {
        CornerRadius::uniform(self as f32)
    }
}
impl IntoProp<CornerRadius> for (f32, f32, f32, f32) {
    /// (top left, top right, bottom left, bottom right)
    fn into_prop(self) -> CornerRadius {
        CornerRadius::new(self.0, self.1, self.2, self.3)
    }
}
impl IntoProp<CornerRadius> for (i32, i32, i32, i32) {
    fn into_prop(self) -> CornerRadius {
        CornerRadius::new(self.0 as f32, self.1 as f32, self.2 as f32, self.3 as f32)
    }
}

/// Polygon and Line points, as ratios of the shape's box.
impl IntoProp<Vec<Point>> for Vec<(f32, f32)> {
    fn into_prop(self) -> Vec<Point> {
        self.into_iter().map(Point::from).collect()
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum GradientType {
    None,
    #[default]
    Linear,
    /// Radial, radius = half of the smaller side.
    Circular,
    /// Radial, stretched to both sides.
    Oval,
    /// Around the center, between the control's `value1` and `value1 + value2` degrees.
    Sweep,
    /// Drawn as Linear, as upstream.
    Conical,
}

/// DrawnUI SkiaGradient. Start and end are ratios of the rect the gradient fills.
#[derive(Clone, PartialEq, Debug)]
pub struct SkiaGradient {
    pub gradient_type: GradientType,
    pub colors: Vec<Color>,
    /// 0..1 per color; used only when there is one per color, else the colors are spread evenly.
    pub color_positions: Vec<f32>,
    /// Linear: the start point. Circular and Oval: the center.
    pub start_x_ratio: f32,
    pub start_y_ratio: f32,
    pub end_x_ratio: f32,
    pub end_y_ratio: f32,
    pub tile_mode: TileMode,
    /// Below 1 darkens the colors, above 1 lightens them.
    pub light: f32,
    /// Multiplies the alpha of every color.
    pub opacity: f32,
    pub blend_mode: BlendMode,
}

impl Default for SkiaGradient {
    fn default() -> Self {
        Self {
            gradient_type: GradientType::Linear,
            colors: Vec::new(),
            color_positions: Vec::new(),
            start_x_ratio: 0.0,
            start_y_ratio: 0.0,
            end_x_ratio: 0.0,
            end_y_ratio: 1.0,
            tile_mode: TileMode::Clamp,
            light: 1.0,
            opacity: 1.0,
            blend_mode: BlendMode::SrcOver,
        }
    }
}

impl SkiaGradient {
    /// Top to bottom for Linear; set the ratios or `angle` for another direction.
    pub fn new(gradient_type: GradientType, colors: impl Into<Vec<Color>>) -> Self {
        Self { gradient_type, colors: colors.into(), ..Self::default() }
    }

    /// Direction of a Linear gradient in degrees: 0 = top to bottom, 90 = left to right, 180 =
    /// bottom to top, 270 = right to left. Sets the start and end ratios (DrawnUI
    /// LinearGradientAngleToPoints).
    pub fn angle(mut self, degrees: f32) -> Self {
        let mut direction = degrees - 90.0;
        if direction < 0.0 {
            direction += 360.0;
        }
        let angle = direction.min(360.0) % 360.0;
        // A direction that points backwards on an axis starts at 0 on it.
        let ratio = |v: f32| if v <= f32::EPSILON { 0.0 } else { v };
        let (start, end) = ((180.0 - angle).to_radians(), (360.0 - angle).to_radians());
        (self.start_x_ratio, self.start_y_ratio) = (ratio(start.cos()), ratio(start.sin()));
        (self.end_x_ratio, self.end_y_ratio) = (ratio(end.cos()), ratio(end.sin()));
        self
    }
}

/// Fluent setters named after the fields: `SkiaShadow::new(color).y(4).blur(6)`.
macro_rules! fluent {
    ($ty:ident { $($name:ident: $field:ty),* $(,)? }) => {
        impl $ty {
            $(pub fn $name(mut self, v: impl IntoProp<$field>) -> Self {
                self.$name = v.into_prop();
                self
            })*
        }
    };
}

fluent!(SkiaGradient {
    gradient_type: GradientType,
    colors: Vec<Color>,
    color_positions: Vec<f32>,
    start_x_ratio: f32,
    start_y_ratio: f32,
    end_x_ratio: f32,
    end_y_ratio: f32,
    tile_mode: TileMode,
    light: f32,
    opacity: f32,
    blend_mode: BlendMode,
});

impl IntoProp<Option<Box<SkiaGradient>>> for SkiaGradient {
    fn into_prop(self) -> Option<Box<SkiaGradient>> {
        Some(Box::new(self))
    }
}

/// DrawnUI SkiaShadow: a blurred copy of the shape behind it. Offsets and blur are points.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct SkiaShadow {
    pub x: f32,
    pub y: f32,
    /// The blur sigma.
    pub blur: f32,
    /// Alpha of the shadow when `color` is fully opaque; a color with its own alpha keeps it.
    pub opacity: f32,
    pub color: Color,
    /// Draws the shadow without the shape.
    pub shadow_only: bool,
}

impl Default for SkiaShadow {
    fn default() -> Self {
        Self { x: 2.0, y: 2.0, blur: 5.0, opacity: 0.5, color: Color::TRANSPARENT, shadow_only: false }
    }
}

impl SkiaShadow {
    /// A shadow of this color with the upstream defaults: 2 points right and down, blur 5,
    /// opacity 0.5.
    pub fn new(color: Color) -> Self {
        Self { color, ..Self::default() }
    }
}

fluent!(SkiaShadow { x: f32, y: f32, blur: f32, opacity: f32, color: Color, shadow_only: bool });

impl IntoProp<Vec<SkiaShadow>> for SkiaShadow {
    fn into_prop(self) -> Vec<SkiaShadow> {
        vec![self]
    }
}