Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/ui/widgets/feathers_gallery.rs
30635 views
1
//! This example shows off the various Bevy Feathers widgets.
2
3
use bevy::{
4
color::palettes,
5
ecs::VariantDefaults,
6
feathers::{
7
constants::{fonts, icons},
8
containers::*,
9
controls::*,
10
cursor::{EntityCursor, OverrideCursor},
11
dark_theme::create_dark_theme,
12
display::{icon, label, label_dim, label_small},
13
font_styles::InheritableFont,
14
palette,
15
rounded_corners::RoundedCorners,
16
theme::{ThemeBackgroundColor, ThemedText, UiTheme},
17
tokens, FeathersPlugins,
18
},
19
input_focus::{tab_navigation::TabGroup, AutoFocus, InputFocus},
20
prelude::*,
21
text::{EditableText, TextEdit, TextEditChange},
22
ui::{Checked, InteractionDisabled},
23
ui_widgets::{
24
checkbox_self_update, radio_self_update, slider_self_update, Activate, ActivateOnPress,
25
RadioGroup, SliderPrecision, SliderStep, SliderValue, ValueChange,
26
},
27
window::SystemCursorIcon,
28
};
29
30
/// A struct to hold the state of various widgets shown in the demo.
31
#[derive(Resource)]
32
struct DemoWidgetStates {
33
rgb_color: Srgba,
34
hsl_color: Hsla,
35
scalar_prop: f32,
36
vec3_prop: Vec3,
37
}
38
39
#[derive(Component, Clone, Copy, PartialEq, FromTemplate)]
40
enum SwatchType {
41
#[default]
42
Rgb,
43
Hsl,
44
}
45
46
#[derive(Component, Clone, Copy, Default)]
47
struct HexColorInput;
48
49
#[derive(Component, Clone, Copy, Default)]
50
struct DemoDisabledButton;
51
52
#[derive(Component, Clone, Copy, Default)]
53
struct DemoScalarField;
54
55
#[derive(Component, Clone, Copy, Default, VariantDefaults)]
56
enum DemoVec3Field {
57
#[default]
58
X,
59
Y,
60
Z,
61
}
62
63
fn main() {
64
App::new()
65
.add_plugins((DefaultPlugins, FeathersPlugins))
66
.insert_resource(UiTheme(create_dark_theme()))
67
.insert_resource(DemoWidgetStates {
68
rgb_color: palettes::tailwind::EMERALD_800.with_alpha(0.7),
69
hsl_color: palettes::tailwind::AMBER_800.into(),
70
scalar_prop: 7.0,
71
vec3_prop: Vec3::new(10.1, 7.124, 100.0),
72
})
73
.add_systems(Startup, scene.spawn())
74
.add_systems(Update, update_colors)
75
.run();
76
}
77
78
fn scene() -> impl SceneList {
79
bsn_list![Camera2d, demo_root()]
80
}
81
82
fn demo_root() -> impl Scene {
83
bsn! {
84
Node {
85
width: percent(100),
86
height: percent(100),
87
align_items: AlignItems::Start,
88
justify_content: JustifyContent::Start,
89
display: Display::Flex,
90
flex_direction: FlexDirection::Row,
91
column_gap: px(8),
92
}
93
TabGroup
94
ThemeBackgroundColor(tokens::WINDOW_BG)
95
Children[
96
:demo_column_1,
97
:demo_column_2,
98
]
99
}
100
}
101
102
fn demo_column_1() -> impl Scene {
103
bsn! {
104
Node {
105
display: Display::Flex,
106
flex_direction: FlexDirection::Column,
107
align_items: AlignItems::Stretch,
108
justify_content: JustifyContent::Start,
109
padding: px(8),
110
row_gap: px(8),
111
width: percent(30),
112
min_width: px(200),
113
}
114
Children [
115
(
116
Node {
117
display: Display::Flex,
118
flex_direction: FlexDirection::Row,
119
align_items: AlignItems::Center,
120
justify_content: JustifyContent::Start,
121
column_gap: px(8),
122
}
123
Children [
124
(
125
@FeathersButton {
126
@caption: {bsn! { Text("Normal") ThemedText }}
127
}
128
Node {
129
flex_grow: 1.0,
130
}
131
AccessibleLabel("Normal")
132
on(|_activate: On<Activate>| {
133
info!("Normal button clicked!");
134
})
135
AutoFocus
136
),
137
(
138
@FeathersButton {
139
@caption: {bsn! { Text("Disabled") ThemedText }},
140
}
141
Node {
142
flex_grow: 1.0,
143
}
144
AccessibleLabel("Disabled")
145
InteractionDisabled
146
DemoDisabledButton
147
on(|_activate: On<Activate>| {
148
info!("Disabled button clicked!");
149
})
150
),
151
(
152
@FeathersButton {
153
@caption: {bsn! { Text("Primary") ThemedText }},
154
@variant: ButtonVariant::Primary,
155
}
156
AccessibleLabel("Primary")
157
Node {
158
flex_grow: 1.0,
159
}
160
on(|_activate: On<Activate>| {
161
info!("Primary button clicked!");
162
})
163
),
164
(
165
@FeathersMenu
166
Children [
167
(
168
@FeathersMenuButton {
169
@caption: {bsn! { Text("Menu") ThemedText }}
170
}
171
AccessibleLabel("Menu Example")
172
Node {
173
flex_grow: 1.0,
174
}
175
),
176
(
177
@FeathersMenuPopup
178
Children [
179
(
180
@FeathersMenuItem {
181
@caption: {bsn! { Text("MenuItem 1") ThemedText }}
182
}
183
on(|_: On<Activate>| {
184
info!("Menu item 1 clicked!");
185
})
186
),
187
(
188
@FeathersMenuItem {
189
@caption: {bsn! { Text("MenuItem 2") ThemedText }}
190
}
191
on(|_: On<Activate>| {
192
info!("Menu item 2 clicked!");
193
})
194
),
195
@FeathersMenuDivider,
196
(
197
@FeathersMenuItem {
198
@caption: {bsn! { Text("MenuItem 3") ThemedText }}
199
}
200
on(|_: On<Activate>| {
201
info!("Menu item 3 clicked!");
202
})
203
)
204
]
205
)
206
]
207
)
208
]
209
),
210
(
211
Node {
212
display: Display::Flex,
213
flex_direction: FlexDirection::Row,
214
align_items: AlignItems::Center,
215
justify_content: JustifyContent::Start,
216
column_gap: px(1),
217
}
218
Children [
219
(
220
@FeathersButton {
221
@caption: {bsn! { Text("Left") ThemedText }},
222
@corners: RoundedCorners::Left,
223
}
224
Node {
225
flex_grow: 1.0,
226
}
227
AccessibleLabel("Left")
228
on(|_activate: On<Activate>| {
229
info!("Left button clicked!");
230
})
231
),
232
(
233
@FeathersButton {
234
@caption: {bsn! { Text("Center") ThemedText }},
235
@corners: RoundedCorners::None,
236
}
237
Node {
238
flex_grow: 1.0,
239
}
240
AccessibleLabel("Center")
241
on(|_activate: On<Activate>| {
242
info!("Center button clicked!");
243
})
244
),
245
(
246
@FeathersButton {
247
@caption: {bsn! { Text("Right") ThemedText }},
248
@variant: ButtonVariant::Primary,
249
@corners: RoundedCorners::Right,
250
}
251
Node {
252
flex_grow: 1.0,
253
}
254
AccessibleLabel("Right")
255
on(|_activate: On<Activate>| {
256
info!("Right button clicked!");
257
})
258
),
259
]
260
),
261
(
262
@FeathersButton
263
on(|_activate: On<Activate>, mut ovr: ResMut<OverrideCursor>| {
264
ovr.0 = if ovr.0.is_some() {
265
None
266
} else {
267
Some(EntityCursor::System(SystemCursorIcon::Wait))
268
};
269
info!("Override cursor button clicked!");
270
})
271
Children [ (Text("Toggle override") ThemedText) ]
272
),
273
(
274
@FeathersCheckbox {
275
@caption: {bsn! { Text("Checkbox") ThemedText }}
276
}
277
Checked
278
AccessibleLabel("Checkbox Example")
279
on(
280
|change: On<ValueChange<bool>>,
281
query: Query<Entity, With<DemoDisabledButton>>,
282
mut commands: Commands| {
283
info!("Checkbox clicked!");
284
let mut button = commands.entity(query.single().unwrap());
285
if change.value {
286
button.insert(InteractionDisabled);
287
} else {
288
button.remove::<InteractionDisabled>();
289
}
290
let mut checkbox = commands.entity(change.source);
291
if change.value {
292
checkbox.insert(Checked);
293
} else {
294
checkbox.remove::<Checked>();
295
}
296
}
297
)
298
),
299
(
300
@FeathersCheckbox {
301
@caption: {bsn! { Text("Fast Click Checkbox") ThemedText }}
302
}
303
ActivateOnPress
304
AccessibleLabel("Fast Click Checkbox Example")
305
on(
306
|change: On<ValueChange<bool>>,
307
mut commands: Commands| {
308
info!("Checkbox clicked!");
309
let mut checkbox = commands.entity(change.source);
310
if change.value {
311
checkbox.insert(Checked);
312
} else {
313
checkbox.remove::<Checked>();
314
}
315
}
316
)
317
),
318
(
319
@FeathersCheckbox {
320
@caption: {bsn! { Text("Disabled") ThemedText }},
321
}
322
InteractionDisabled
323
AccessibleLabel("Disabled Checkbox Example")
324
on(|_change: On<ValueChange<bool>>| {
325
warn!("Disabled checkbox clicked!");
326
})
327
),
328
(
329
@FeathersCheckbox {
330
@caption: {bsn! { Text("Checked+Disabled") ThemedText }}
331
}
332
InteractionDisabled
333
Checked
334
AccessibleLabel("Disabled and Checked Checkbox Example")
335
on(|_change: On<ValueChange<bool>>| {
336
warn!("Disabled checkbox clicked!");
337
})
338
),
339
(
340
Node {
341
display: Display::Flex,
342
flex_direction: FlexDirection::Row,
343
align_items: AlignItems::Center,
344
justify_content: JustifyContent::Start,
345
column_gap: px(8),
346
}
347
Children [
348
(
349
Node {
350
display: Display::Flex,
351
flex_direction: FlexDirection::Column,
352
row_gap: px(4),
353
}
354
RadioGroup
355
on(radio_self_update)
356
Children [
357
(
358
@FeathersRadio {
359
@caption: {bsn! { Text("One") ThemedText }}
360
}
361
Checked
362
),
363
@FeathersRadio {
364
@caption: {bsn! { Text("Two") ThemedText }}
365
},
366
(
367
@FeathersRadio {
368
@caption: {bsn! { Text("Fast Click") ThemedText }}
369
}
370
ActivateOnPress
371
),
372
(
373
@FeathersRadio {
374
@caption: {bsn! { Text("Disabled") ThemedText }}
375
}
376
InteractionDisabled
377
),
378
]
379
)
380
]
381
),
382
(
383
Node {
384
display: Display::Flex,
385
flex_direction: FlexDirection::Row,
386
align_items: AlignItems::Center,
387
justify_content: JustifyContent::Start,
388
column_gap: px(8),
389
}
390
Children [
391
(@FeathersToggleSwitch on(checkbox_self_update)),
392
(@FeathersToggleSwitch ActivateOnPress on(checkbox_self_update)),
393
(@FeathersToggleSwitch InteractionDisabled on(checkbox_self_update)),
394
(@FeathersToggleSwitch InteractionDisabled Checked on(checkbox_self_update)),
395
(@FeathersDisclosureToggle on(checkbox_self_update)),
396
]
397
),
398
(
399
@FeathersSlider {
400
@max: 100.0,
401
@value: 20.0,
402
}
403
SliderStep(10.)
404
SliderPrecision(2)
405
on(slider_self_update)
406
),
407
(
408
Node {
409
display: Display::Flex,
410
flex_direction: FlexDirection::Row,
411
align_items: AlignItems::Center,
412
justify_content: JustifyContent::SpaceBetween,
413
column_gap: px(4),
414
}
415
Children [
416
label("Srgba"),
417
// Spacer
418
:flex_spacer,
419
// Text input
420
(
421
@FeathersTextInputContainer
422
Node {
423
flex_grow: 0.
424
padding: { px(4).left() },
425
}
426
Children [
427
(
428
@FeathersTextInput {
429
@visible_width: 10f32,
430
@max_characters: 9usize,
431
}
432
InheritableFont {
433
font: fonts::MONO
434
}
435
HexColorInput
436
on(handle_hex_color_change)
437
)
438
]
439
)
440
(@FeathersColorSwatch SwatchType::Rgb),
441
]
442
),
443
(
444
@FeathersColorPlane::RedBlue
445
on(|change: On<ValueChange<Vec2>>, mut color: ResMut<DemoWidgetStates>| {
446
color.rgb_color.red = change.value.x;
447
color.rgb_color.blue = change.value.y;
448
})
449
),
450
(
451
@FeathersColorSlider {
452
@value: 0.5,
453
@channel: ColorChannel::Red
454
}
455
AccessibleLabel("Red Channel")
456
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
457
color.rgb_color.red = change.value;
458
})
459
),
460
(
461
@FeathersColorSlider {
462
@value: 0.5,
463
@channel: ColorChannel::Green
464
}
465
AccessibleLabel("Green Channel")
466
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
467
color.rgb_color.green = change.value;
468
})
469
),
470
(
471
@FeathersColorSlider {
472
@value: 0.5,
473
@channel: ColorChannel::Blue
474
}
475
AccessibleLabel("Blue Channel")
476
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
477
color.rgb_color.blue = change.value;
478
})
479
),
480
(
481
@FeathersColorSlider {
482
@value: 0.5,
483
@channel: ColorChannel::Alpha
484
}
485
AccessibleLabel("Alpha Channel")
486
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
487
color.rgb_color.alpha = change.value;
488
})
489
),
490
(
491
Node {
492
display: Display::Flex,
493
align_items: AlignItems::Center,
494
flex_direction: FlexDirection::Row,
495
justify_content: JustifyContent::SpaceBetween,
496
}
497
Children [
498
label("Hsl"),
499
(@FeathersColorSwatch SwatchType::Hsl)
500
]
501
),
502
(
503
@FeathersColorSlider {
504
@value: 0.5,
505
@channel: ColorChannel::HslHue
506
}
507
AccessibleLabel("Hue Channel")
508
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
509
color.hsl_color.hue = change.value;
510
})
511
),
512
(
513
@FeathersColorSlider {
514
@value: 0.5,
515
@channel: ColorChannel::HslSaturation
516
}
517
AccessibleLabel("Saturation Channel")
518
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
519
color.hsl_color.saturation = change.value;
520
})
521
),
522
(
523
@FeathersColorSlider {
524
@value: 0.5,
525
@channel: ColorChannel::HslLightness
526
}
527
AccessibleLabel("Lightness Channel")
528
on(|change: On<ValueChange<f32>>, mut color: ResMut<DemoWidgetStates>| {
529
color.hsl_color.lightness = change.value;
530
})
531
)
532
]
533
}
534
}
535
536
fn demo_column_2() -> impl Scene {
537
bsn! {
538
Node {
539
display: Display::Flex,
540
flex_direction: FlexDirection::Column,
541
align_items: AlignItems::Stretch,
542
justify_content: JustifyContent::Start,
543
padding: px(8),
544
row_gap: px(8),
545
width: percent(30),
546
min_width: px(200),
547
}
548
Children [
549
(
550
:pane Children [
551
:pane_header Children [
552
@FeathersToolButton {
553
@variant: ButtonVariant::Primary,
554
} Children [
555
(Text("\u{0398}") ThemedText)
556
],
557
:pane_header_divider,
558
@FeathersToolButton {
559
@variant: ButtonVariant::Plain,
560
} Children [
561
(Text("\u{00BC}") ThemedText)
562
],
563
@FeathersToolButton {
564
@variant: ButtonVariant::Plain,
565
} Children [
566
(Text("\u{00BD}") ThemedText)
567
],
568
@FeathersToolButton {
569
@variant: ButtonVariant::Plain,
570
} Children [
571
(Text("\u{00BE}") ThemedText)
572
],
573
:pane_header_divider,
574
@FeathersToolButton {
575
@variant: ButtonVariant::Plain,
576
} Children [
577
icon(icons::CHEVRON_DOWN)
578
],
579
:flex_spacer,
580
@FeathersToolButton {
581
@variant: ButtonVariant::Plain,
582
} Children [
583
icon(icons::X)
584
],
585
],
586
(
587
:pane_body Children [
588
label_dim("A standard editor pane"),
589
:subpane Children [
590
:subpane_header Children [
591
(Text("Left") ThemedText),
592
(Text("Center") ThemedText),
593
(Text("Right") ThemedText)
594
],
595
:subpane_body Children [
596
label_dim("A standard sub-pane"),
597
:group
598
Children [
599
:group_header Children [
600
(Text("Group") ThemedText),
601
],
602
:group_body
603
Children [
604
label("A standard group"),
605
label_small("Scalar property"),
606
(
607
:@FeathersNumberInput
608
DemoScalarField
609
Node {
610
flex_grow: 1.0,
611
max_width: px(100),
612
}
613
on(
614
|value_change: On<ValueChange<f32>>,
615
mut states: ResMut<DemoWidgetStates>| {
616
if value_change.is_final {
617
states.scalar_prop = value_change.value;
618
}
619
})
620
),
621
label_small("Scalar property (copy)"),
622
(
623
:@FeathersNumberInput
624
DemoScalarField
625
Node {
626
flex_grow: 1.0,
627
max_width: px(100),
628
}
629
on(
630
|value_change: On<ValueChange<f32>>,
631
mut states: ResMut<DemoWidgetStates>| {
632
if value_change.is_final {
633
states.scalar_prop = value_change.value;
634
}
635
})
636
),
637
label_small("Vec3 property"),
638
Node {
639
display: Display::Flex,
640
flex_direction: FlexDirection::Row,
641
column_gap: px(6),
642
align_items: AlignItems::Center,
643
justify_content: JustifyContent::SpaceBetween,
644
}
645
Children [
646
(
647
@FeathersNumberInput {
648
@sigil_color: tokens::TEXT_INPUT_X_AXIS,
649
@label_text: "X",
650
}
651
DemoVec3Field::X
652
Node {
653
flex_grow: 1.0,
654
}
655
BorderColor::all(palette::X_AXIS)
656
on(
657
|value_change: On<ValueChange<f32>>,
658
mut states: ResMut<DemoWidgetStates>| {
659
if value_change.is_final {
660
states.vec3_prop.x = value_change.value;
661
}
662
})
663
),
664
(
665
@FeathersNumberInput {
666
@sigil_color: tokens::TEXT_INPUT_Y_AXIS,
667
@label_text: "Y",
668
}
669
DemoVec3Field::Y
670
Node {
671
flex_grow: 1.0,
672
}
673
on(
674
|value_change: On<ValueChange<f32>>,
675
mut states: ResMut<DemoWidgetStates>| {
676
if value_change.is_final {
677
states.vec3_prop.y = value_change.value;
678
}
679
})
680
),
681
(
682
@FeathersNumberInput {
683
@sigil_color: tokens::TEXT_INPUT_Z_AXIS,
684
@label_text: "Z",
685
}
686
DemoVec3Field::Z
687
Node {
688
flex_grow: 1.0,
689
}
690
on(
691
|value_change: On<ValueChange<f32>>,
692
mut states: ResMut<DemoWidgetStates>| {
693
if value_change.is_final {
694
states.vec3_prop.z = value_change.value;
695
}
696
})
697
),
698
],
699
],
700
]
701
],
702
]
703
]
704
),
705
]
706
),
707
]
708
}
709
}
710
711
fn update_colors(
712
states: Res<DemoWidgetStates>,
713
mut sliders: Query<(Entity, &ColorSlider, &mut SliderBaseColor)>,
714
mut swatches: Query<(&mut ColorSwatchValue, &SwatchType), With<FeathersColorSwatch>>,
715
mut color_planes: Query<&mut ColorPlaneValue, With<FeathersColorPlane>>,
716
q_text_input: Single<(Entity, &mut EditableText), With<HexColorInput>>,
717
q_scalar_input: Query<Entity, With<DemoScalarField>>,
718
q_vec3_input: Query<(Entity, &DemoVec3Field)>,
719
mut commands: Commands,
720
focus: Res<InputFocus>,
721
) {
722
if states.is_changed() {
723
for (slider_ent, slider, mut base) in sliders.iter_mut() {
724
match slider.channel {
725
ColorChannel::Red => {
726
base.0 = states.rgb_color.into();
727
commands
728
.entity(slider_ent)
729
.insert(SliderValue(states.rgb_color.red));
730
}
731
ColorChannel::Green => {
732
base.0 = states.rgb_color.into();
733
commands
734
.entity(slider_ent)
735
.insert(SliderValue(states.rgb_color.green));
736
}
737
ColorChannel::Blue => {
738
base.0 = states.rgb_color.into();
739
commands
740
.entity(slider_ent)
741
.insert(SliderValue(states.rgb_color.blue));
742
}
743
ColorChannel::HslHue => {
744
base.0 = states.hsl_color.into();
745
commands
746
.entity(slider_ent)
747
.insert(SliderValue(states.hsl_color.hue));
748
}
749
ColorChannel::HslSaturation => {
750
base.0 = states.hsl_color.into();
751
commands
752
.entity(slider_ent)
753
.insert(SliderValue(states.hsl_color.saturation));
754
}
755
ColorChannel::HslLightness => {
756
base.0 = states.hsl_color.into();
757
commands
758
.entity(slider_ent)
759
.insert(SliderValue(states.hsl_color.lightness));
760
}
761
ColorChannel::Alpha => {
762
base.0 = states.rgb_color.into();
763
commands
764
.entity(slider_ent)
765
.insert(SliderValue(states.rgb_color.alpha));
766
}
767
}
768
}
769
770
for (mut swatch_value, swatch_type) in swatches.iter_mut() {
771
swatch_value.0 = match swatch_type {
772
SwatchType::Rgb => states.rgb_color.into(),
773
SwatchType::Hsl => states.hsl_color.into(),
774
};
775
}
776
777
for mut plane_value in color_planes.iter_mut() {
778
plane_value.0.x = states.rgb_color.red;
779
plane_value.0.y = states.rgb_color.blue;
780
plane_value.0.z = states.rgb_color.green;
781
}
782
783
// Only update the hex input field when it's not focused, otherwise it interferes
784
// with typing.
785
let (input_ent, mut editable_text) = q_text_input.into_inner();
786
if Some(input_ent) != focus.get() {
787
editable_text.queue_edit(TextEdit::SelectAll);
788
editable_text.queue_edit(TextEdit::Insert(states.rgb_color.to_hex().into()));
789
}
790
791
for scalar_input_ent in q_scalar_input.iter() {
792
commands.trigger(UpdateNumberInput {
793
entity: scalar_input_ent,
794
value: NumberInputValue::F32(states.scalar_prop),
795
});
796
}
797
798
for (vec3_input_ent, axis) in q_vec3_input.iter() {
799
let new_value = match axis {
800
DemoVec3Field::X => states.vec3_prop.x,
801
DemoVec3Field::Y => states.vec3_prop.y,
802
DemoVec3Field::Z => states.vec3_prop.z,
803
};
804
805
commands.trigger(UpdateNumberInput {
806
entity: vec3_input_ent,
807
value: NumberInputValue::F32(new_value),
808
});
809
}
810
}
811
}
812
813
fn handle_hex_color_change(
814
_change: On<TextEditChange>,
815
q_text_input: Single<&EditableText, With<HexColorInput>>,
816
mut colors: ResMut<DemoWidgetStates>,
817
) {
818
let editable_text = *q_text_input;
819
if let Ok(color) = Srgba::hex(editable_text.value().to_string())
820
&& color != colors.rgb_color
821
{
822
colors.rgb_color = color;
823
}
824
}
825
826