woodpecker_ui 0.1.1

A UI library for the Bevy game engine.
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
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
use bevy::prelude::*;
use bevy_vello::prelude::VelloFont;
pub use corner::Corner;
pub use edge::Edge;
pub use layout::*;
pub use units::Units;

use crate::font::TextAlign;

mod corner;
mod edge;
mod layout;
mod units;

/// A struct used to pass styles into a widget.
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq)]
#[reflect(Component)]
pub struct WoodpeckerStyleProp(pub WoodpeckerStyle);

/// Text wrapping mode
#[derive(Debug, Eq, Default, PartialEq, Reflect, Clone, Copy)]
pub enum TextWrap {
    /// No wrapping
    None,
    /// Wraps at a glyph level
    Glyph,
    /// Wraps at the word level
    Word,
    #[default]
    /// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself
    WordOrGlyph,
}

// A struct used to define the look of a widget
///
/// All fields are `pub`, so you can simply define your styles.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Copy)]
#[reflect(Component)]
pub struct WoodpeckerStyle {
    /************************ Layout ************************/
    /// The height of this widget
    pub height: Units,
    /// The maximum height of this widget
    pub max_height: Units,
    /// The maximum width of this widget
    pub max_width: Units,
    /// The minimum height of this widget
    pub min_height: Units,
    /// The minimum width of this widget
    pub min_width: Units,
    /// The inner padding between the edges of this widget and its children
    pub padding: Edge,
    /// The width of this widget
    pub width: Units,
    /// Sets the layout used for the children of this node
    ///
    /// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None.
    pub display: WidgetDisplay,
    /// Show or hide an element without changing layout
    pub visibility: WidgetVisibility,
    /// How children overflowing their container should affect layout
    ///
    /// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should
    /// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout,
    /// the main ones being:
    ///
    ///   - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based
    ///   - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property)
    ///
    /// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for
    /// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`.
    ///
    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
    pub overflow: WidgetOverflow,
    /// The positioning strategy for this item.
    ///
    /// This controls both how the origin is determined for the [`WoodpeckerStyle::position`] field,
    /// and whether or not the item will be controlled by flexbox's layout algorithm.
    ///
    /// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
    /// which can be unintuitive.
    ///
    /// [`WidgetPosition::Relative`] is the default value, in contrast to the default behavior in CSS.
    pub position: WidgetPosition,
    /// Position Left
    pub left: Units,
    /// Position Left
    pub right: Units,
    /// Position Left
    pub top: Units,
    /// Position Left
    pub bottom: Units,
    /// How large should the margin be on each side?
    pub margin: Edge,
    /// Used to control how child nodes are aligned.
    /// For Flexbox it controls alignment in the cross axis
    /// For Grid it controls alignment in the block axis
    ///
    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
    pub align_items: Option<WidgetAlignItems>,
    /// Used to control how the specified nodes is aligned.
    /// Overrides the parent Node's `AlignItems` property.
    /// For Flexbox it controls alignment in the cross axis
    /// For Grid it controls alignment in the block axis
    ///
    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
    pub align_self: Option<WidgetAlignSelf>,
    /// Used to control how child nodes are aligned.
    /// Does not apply to Flexbox, and will be ignored if specified on a flex container
    /// For Grid it controls alignment in the inline axis
    ///
    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
    pub justify_items: Option<WidgetJustifyItems>,
    /// Used to control how the specified nodes is aligned.
    /// Overrides the parent Node's `JustifyItems` property.
    /// Does not apply to Flexbox, and will be ignored if specified on a flex child
    /// For Grid it controls alignment in the inline axis
    ///
    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
    pub justify_self: Option<WidgetJustifySelf>,
    /// Sets the distribution of space between and around content items
    /// For Flexbox it controls alignment in the cross axis
    /// For Grid it controls alignment in the block axis
    ///
    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
    pub align_content: Option<WidgetAlignContent>,
    /// Sets the distribution of space between and around content items
    /// For Flexbox it controls alignment in the main axis
    /// For Grid it controls alignment in the inline axis
    ///
    /// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
    pub justify_content: Option<WidgetJustifyContent>,
    /// How large should the gaps between items in a grid or flex container be?
    pub gap: (Units, Units),
    /// The direction of the flexbox layout main axis.
    ///
    /// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
    /// Adding items will cause them to be positioned adjacent to each other along the main axis.
    /// By varying this value throughout your tree, you can create complex axis-aligned layouts.
    ///
    /// Items are always aligned relative to the cross axis, and justified relative to the main axis.
    ///
    /// The default behavior is [`FlexDirection::Row`].
    ///
    /// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
    pub flex_direction: WidgetFlexDirection,
    /// Controls whether flex items are forced onto one line or can wrap onto multiple lines.
    ///
    /// Defaults to [`FlexWrap::NoWrap`]
    ///
    /// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
    pub flex_wrap: WidgetFlexWrap,
    /// Sets the initial main axis size of the item
    pub flex_basis: Units,
    /// The relative rate at which this item grows when it is expanding to fill space
    ///
    /// 0.0 is the default value, and this value must be positive.
    pub flex_grow: f32,
    /// The relative rate at which this item shrinks when it is contracting to fit into space
    ///
    /// 1.0 is the default value, and this value must be positive.
    pub flex_shrink: f32,
    // TODO: Add grid support..
    /************************ Rendering ************************/
    /// The background color of this widget
    ///
    /// Only applies to widgets marked [`crate::prelude::WidgetRender::Quad`]
    pub background_color: Color,
    /// The color of the border around this widget
    ///
    /// Currently, this controls all border sides.
    ///
    /// Only applies to widgets marked [`crate::prelude::WidgetRender::Quad`]
    pub border_color: Color,
    /// The radius of the corners (in pixels)
    ///
    /// The order is (Top, Right, Bottom, Left).
    ///
    /// Only applies to widgets with [`crate::prelude::WidgetRender::Quad`]
    pub border_radius: Corner,
    /// The widths of the borders (in pixels)
    ///
    /// The order is (Top, Right, Bottom, Left).
    ///
    /// Only applies to widgets with [`crate::prelude::WidgetRender::Quad`]
    pub border: Edge,
    /// The text color for this widget
    ///
    /// Only applies to widgets with the [`crate::prelude::WidgetRender::Text`] component.
    pub color: Color,
    /// Font handle if none is set the [`crate::DefaultFont`] is used.
    /// We use AssetId here because it can be copied thus it makes styles easier.
    pub font: Option<AssetId<VelloFont>>,
    /// The font size for this widget, in pixels
    ///
    /// Only applies to [`crate::prelude::WidgetRender::Text`]
    pub font_size: f32,
    /// The text wrap mode used.
    pub text_wrap: TextWrap,
    /// The layout method for children of this widget
    /// The line height for this widget, in pixels
    pub line_height: Option<f32>,
    /// The opacity of the widget and it's children
    /// Note: This will spawn a new UI render layer so use sparingly.
    pub opacity: f32,
    /// Alignent for text rendering
    /// If none is set it uses right for RTL and left for LTR text.
    pub text_alignment: Option<TextAlign>,
    /// Image Quality
    pub image_quality: ImageQuality,
    /// Z Index
    pub z_index: Option<WidgetZ>,
}

/// A z index which is either global or relative.
#[derive(Reflect, Debug, Clone, PartialEq, Copy)]
pub enum WidgetZ {
    /// Global to the entire widget tree
    Global(u32),
    /// Relative to the order of its sibilings.
    Relative(i32),
}

impl WidgetZ {
    /// Retrieves the global z index or returns none.
    pub fn get_global(&self) -> Option<u32> {
        match self {
            WidgetZ::Global(z) => Some(*z),
            WidgetZ::Relative(_) => None,
        }
    }

    /// Retrieves the relative z index or returns none.
    pub fn get_relative(&self) -> Option<i32> {
        match self {
            WidgetZ::Global(_) => None,
            WidgetZ::Relative(z) => Some(*z),
        }
    }
}

/// Image Quality
#[derive(Reflect, Debug, Default, Clone, PartialEq, Copy)]
pub enum ImageQuality {
    /// Nearest Neighbor Filtering
    Low,
    /// Bilinear Filtering
    #[default]
    Medium,
    /// Bicubic Filtering
    High,
}

impl Into<bevy_vello::vello::peniko::ImageQuality> for ImageQuality {
    fn into(self) -> bevy_vello::vello::peniko::ImageQuality {
        match self {
            ImageQuality::Low => bevy_vello::vello::peniko::ImageQuality::Low,
            ImageQuality::Medium => bevy_vello::vello::peniko::ImageQuality::Medium,
            ImageQuality::High => bevy_vello::vello::peniko::ImageQuality::High,
        }
    }
}

impl Default for WoodpeckerStyle {
    fn default() -> Self {
        WoodpeckerStyle::DEFAULT
    }
}

impl WoodpeckerStyle {
    /// Same as Default::default but constant.
    pub const DEFAULT: WoodpeckerStyle = WoodpeckerStyle {
        width: Units::Auto,
        height: Units::Auto,
        max_height: Units::Auto,
        max_width: Units::Auto,
        min_height: Units::Auto,
        min_width: Units::Auto,
        padding: Edge {
            left: Units::Pixels(0.0),
            right: Units::Pixels(0.0),
            top: Units::Pixels(0.0),
            bottom: Units::Pixels(0.0),
        },
        display: WidgetDisplay::Flex,
        visibility: WidgetVisibility::Visible,
        overflow: WidgetOverflow::Visible,
        position: WidgetPosition::Relative,
        left: Units::Auto,
        right: Units::Auto,
        top: Units::Auto,
        bottom: Units::Auto,
        margin: Edge {
            left: Units::Pixels(0.0),
            right: Units::Pixels(0.0),
            top: Units::Pixels(0.0),
            bottom: Units::Pixels(0.0),
        },
        align_items: None,
        align_self: None,
        justify_items: None,
        justify_self: None,
        align_content: None,
        justify_content: None,
        gap: (Units::Pixels(0.0), Units::Pixels(0.0)),
        flex_direction: WidgetFlexDirection::Row,
        flex_wrap: WidgetFlexWrap::NoWrap,
        flex_basis: Units::Auto,
        flex_grow: 0.0,
        flex_shrink: 1.0,
        background_color: Color::Srgba(Srgba {
            red: 0.0,
            green: 0.0,
            blue: 0.0,
            alpha: 0.0,
        }),
        border_color: Color::Srgba(Srgba {
            red: 0.0,
            green: 0.0,
            blue: 0.0,
            alpha: 0.0,
        }),
        border_radius: Corner {
            top_left: Units::Pixels(0.0),
            top_right: Units::Pixels(0.0),
            bottom_left: Units::Pixels(0.0),
            bottom_right: Units::Pixels(0.0),
        },
        border: Edge {
            left: Units::Pixels(0.0),
            right: Units::Pixels(0.0),
            top: Units::Pixels(0.0),
            bottom: Units::Pixels(0.0),
        },
        color: Color::WHITE,
        font_size: 18.0,
        line_height: None,
        opacity: 1.0,
        font: None,
        text_wrap: TextWrap::WordOrGlyph,
        text_alignment: None,
        image_quality: ImageQuality::Medium,
        z_index: None,
    };

    /// Lerps between two styles.
    ///
    /// Note: Only lerps: border_color, color, font_size, height, max_height, width,
    /// max_width, min_width, min_height, left, bottom, right, top, and opacity currrently.
    pub fn lerp(&self, b: &WoodpeckerStyle, x: f32) -> WoodpeckerStyle {
        let mut new_styles = *self; // Default to A styles.

        new_styles.background_color = hsv_lerp(&self.background_color, &b.background_color, x);

        // new_styles.border = Edge::new(
        //     lerp_units(self.border.top, b.top, x),
        //     lerp_units(self.border.right, b.right, x),
        //     lerp_units(self.border.bottom, b.bottom, x),
        //     lerp_units(self.border.left, b.left, x),
        // );

        new_styles.border_color = hsv_lerp(&self.border_color, &b.border_color, x);

        // new_styles.border_radius = Corner::new(
        //     lerp_units(self.border_radius.top_left, b.border_radius.top_left, x),
        //     lerp_units(self.border_radius.top_right, b.border_radius.top_right, x),
        //     lerp_units(
        //         self.border_radius.bottom_left,
        //         b.border_radius.bottom_left,
        //         x,
        //     ),
        //     lerp_units(
        //         self.border_radius.bottom_right,
        //         b.border_radius.bottom_right,
        //         x,
        //     ),
        // );

        new_styles.color = hsv_lerp(&self.color, &b.color, x);

        new_styles.font_size = lerp(self.font_size, b.font_size, x);
        new_styles.height = lerp_units(self.height, b.height, x);
        new_styles.max_height = lerp_units(self.max_height, b.max_height, x);
        new_styles.max_width = lerp_units(self.max_width, b.max_width, x);
        new_styles.min_height = lerp_units(self.min_height, b.min_height, x);
        new_styles.min_width = lerp_units(self.min_width, b.min_width, x);

        // new_styles.padding = Edge::new(
        //     lerp_units(self.padding.top, b.padding.top, x),
        //     lerp_units(self.padding.right, b.padding.right, x),
        //     lerp_units(self.padding.bottom, b.padding.bottom, x),
        //     lerp_units(self.padding.left, b.padding.left, x),
        // );

        new_styles.left = lerp_units(self.left, b.left, x);
        new_styles.right = lerp_units(self.right, b.right, x);
        new_styles.top = lerp_units(self.top, b.top, x);
        new_styles.bottom = lerp_units(self.bottom, b.bottom, x);
        new_styles.width = lerp_units(self.width, b.width, x);
        new_styles.opacity = lerp(self.opacity, b.opacity, x);

        new_styles
    }
}

impl From<&WoodpeckerStyle> for taffy::Style {
    fn from(val: &WoodpeckerStyle) -> taffy::Style {
        (*val).into()
    }
}

impl From<WoodpeckerStyle> for taffy::Style {
    fn from(val: WoodpeckerStyle) -> taffy::Style {
        taffy::Style {
            display: val.display.into(),
            overflow: taffy::Point {
                x: val.overflow.into(),
                y: val.overflow.into(),
            },
            position: val.position.into(),
            inset: Edge::new(val.top, val.right, val.bottom, val.left).into(),
            size: taffy::Size {
                width: val.width.into(),
                height: val.height.into(),
            },
            min_size: taffy::Size {
                width: val.min_width.into(),
                height: val.min_height.into(),
            },
            max_size: taffy::Size {
                width: val.max_width.into(),
                height: val.max_height.into(),
            },
            margin: val.margin.into(),
            padding: val.padding.into(),
            border: val.border.into(),
            align_items: val.align_items.map(|i| i.into()),
            align_self: val.align_self.map(|i| i.into()),
            justify_items: val.justify_items.map(|i| i.into()),
            justify_self: val.justify_self.map(|i| i.into()),
            align_content: val.align_content.map(|i| i.into()),
            justify_content: val.justify_content.map(|i| i.into()),
            gap: taffy::Size {
                width: val.gap.0.into(),
                height: val.gap.1.into(),
            },
            flex_direction: val.flex_direction.into(),
            flex_wrap: val.flex_wrap.into(),
            flex_basis: val.flex_basis.into(),
            flex_grow: val.flex_grow,
            flex_shrink: val.flex_shrink,
            ..Default::default()
        }
    }
}

fn lerp_units(prop_a: Units, prop_b: Units, x: f32) -> Units {
    match (prop_a, prop_b) {
        (Units::Pixels(a), Units::Pixels(b)) => {
            let mut value = lerp(a, b, x);
            if a > b {
                value = value.clamp(b, a);
            } else {
                value = value.clamp(a, b);
            }
            Units::Pixels(value)
        }
        (Units::Percentage(a), Units::Percentage(b)) => Units::Percentage(if a > b {
            lerp(a, b, x).clamp(b, a)
        } else {
            lerp(a, b, x).clamp(a, b)
        }),
        _ => {
            bevy::prelude::trace!(
                "Cannot lerp between non-matching units! Unit_A: {:?}, Unit_B: {:?}",
                prop_a,
                prop_b
            );
            prop_b
        }
    }
}

// fn lerp_ang(a: f32, b: f32, x: f32) -> f32 {
//     let ang = ((((a - b) % std::f32::consts::TAU) + std::f32::consts::PI * 3.)
//         % std::f32::consts::TAU)
//         - std::f32::consts::PI;
//     ang * x + b
// }

fn rgb_to_hsv(from: Srgba) -> Vec3 {
    // xyz <-> hsv
    let r = from.red;
    let g = from.green;
    let b = from.blue;

    let mut res = Vec3::ZERO;

    let min = r.min(g).min(b);
    let max = r.max(g).max(b);

    // Value
    res.z = max;

    let delta = max - min;
    // calc Saturation
    if max != 0.0 {
        res.y = delta / max;
    } else {
        res.x = -1.0;
        res.y = 0.0;

        return res;
    }

    // calc Hue
    if r == max {
        // between Yellow & Magenta
        res.x = (g - b) / delta;
    } else if g == max {
        // cyan to yellow
        res.x = 2.0 + (b - r) / delta;
    } else {
        // b == max // Megnta to cyan
        res.x = 4.0 + (r - g) / delta;
    }

    res.x *= 60.0; // Convert to degrees
    if res.x < 0.0 {
        res.x += 360.0; // Unwrap angle in case of negative
    }

    res
}

fn hsv_to_rgb(from: &Vec3) -> Srgba {
    let h = from.x;
    let s = from.y;
    let v = from.z;

    // Calc base values
    let c = s * v;
    let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
    let m = v - c;

    let mut res = Vec4::new(0.0, 0.0, 0.0, 1.0);

    if (0.0..60.0).contains(&h) {
        res.x = c;
        res.y = x;
        res.z = 0.0;
    } else if (60.0..120.0).contains(&h) {
        res.x = x;
        res.y = c;
        res.z = 0.0;
    } else if (120.0..180.0).contains(&h) {
        res.x = 0.0;
        res.y = c;
        res.z = x;
    } else if (180.0..240.0).contains(&h) {
        res.x = 0.0;
        res.y = x;
        res.z = c;
    } else if (240.0..300.0).contains(&h) {
        res.x = x;
        res.y = 0.0;
        res.z = c;
    } else {
        res.x = c;
        res.y = 0.0;
        res.z = x;
    }

    res += Vec4::new(m, m, m, 0.0);

    Srgba::from_f32_array(res.to_array())
}

fn hsv_lerp(from: &Color, to: &Color, amount: f32) -> Color {
    let from_a = from.alpha();
    let to_a = to.alpha();
    let from = rgb_to_hsv(from.to_srgba());
    let to = rgb_to_hsv(to.to_srgba());
    let mut res = from.lerp(to, amount);

    if from.x < 0.0 {
        res.x = to.x;
    }
    let mut color: Color = hsv_to_rgb(&res).into();
    color.set_alpha(lerp(from_a, to_a, amount).clamp(0.0, 1.0));
    color
}

pub(crate) fn lerp(a: f32, b: f32, x: f32) -> f32 {
    a * (1.0 - x) + b * x
}