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