Skip to main content

headless_tabs/
headless_tabs.rs

1//! Demonstrates the behavior-only tab widgets in `bevy_ui_widgets`.
2
3use bevy::{
4    ecs::template::{EntityTemplate, OptionTemplate},
5    input_focus::{
6        tab_navigation::{TabGroup, TabNavigationPlugin},
7        InputFocus, InputFocusVisible,
8    },
9    picking::hover::Hovered,
10    prelude::*,
11    ui::{InteractionDisabled, Selected},
12    ui_widgets::{
13        tablist_self_update, ControlOrientation, SelectedTab, Tab, TabActivation, TabList,
14        ValueChange,
15    },
16};
17
18#[derive(Component, Default, Clone)]
19struct ShowcaseTab;
20
21#[derive(Resource, Default)]
22struct ControlledSelection(Option<Entity>);
23
24fn main() {
25    App::new()
26        .add_plugins((DefaultPlugins, TabNavigationPlugin))
27        .init_resource::<ControlledSelection>()
28        .add_systems(Startup, showcase.spawn())
29        .add_systems(Update, update_tab_styles)
30        .run();
31}
32
33fn showcase() -> impl SceneList {
34    bsn_list! {
35        Camera2d
36        --
37        Node {
38            width: percent(100),
39            height: percent(100),
40            padding: UiRect::all(px(24)),
41            display: Display::Flex,
42            flex_direction: FlexDirection::Column,
43            align_items: AlignItems::Start,
44            row_gap: px(18),
45            overflow: Overflow::scroll_y(),
46        }
47        BackgroundColor(Color::srgb(0.06, 0.07, 0.09))
48        TabGroup
49        Children [
50            @section_label("Horizontal automatic - self-updating")
51            --
52            #automatic
53            @tab_strip(ControlOrientation::Horizontal)
54            TabList {
55                orientation: ControlOrientation::Horizontal,
56                activation: TabActivation::Automatic,
57            }
58            @selected_tab(#automatic_general)
59            on(tablist_self_update)
60            Children [
61                #automatic_general
62                @tab_header("General")
63                --
64                @tab_header("Rendering")
65                --
66                @tab_header("Disabled")
67                InteractionDisabled
68            ]
69            --
70            @section_label("Horizontal manual - focus and selection are separate")
71            --
72            @tab_strip(ControlOrientation::Horizontal)
73            TabList
74            @selected_tab(#manual_scene)
75            on(tablist_self_update)
76            Children [
77                #manual_scene @tab_header("Scene")
78                --
79                @tab_header("Assets")
80                --
81                @tab_header("Inspector")
82            ]
83            --
84            @section_label("Vertical manual")
85            --
86            @tab_strip(ControlOrientation::Vertical)
87            TabList {
88                orientation: ControlOrientation::Vertical,
89                activation: TabActivation::Manual,
90            }
91            @selected_tab(#vertical_transform)
92            on(tablist_self_update)
93            Children [
94                #vertical_transform @tab_header("Transform")
95                --
96                @tab_header("Visibility")
97                --
98                @tab_header("Metadata")
99            ]
100            --
101            @section_label("Controlled - observer updates external state")
102            --
103            @tab_strip(ControlOrientation::Horizontal)
104            TabList
105            @selected_tab(#controlled_a)
106            on(controlled_selection)
107            Children [
108                #controlled_a @tab_header("External A")
109                --
110                @tab_header("External B")
111            ]
112        ]
113    }
114}
115
116fn tab_strip(orientation: ControlOrientation) -> impl Scene {
117    let flex_direction = match orientation {
118        ControlOrientation::Horizontal => FlexDirection::Row,
119        ControlOrientation::Vertical => FlexDirection::Column,
120    };
121    bsn! {
122        Node {
123            display: Display::Flex,
124            flex_direction,
125            align_items: AlignItems::Stretch,
126        }
127        BackgroundColor(Color::srgb(0.10, 0.11, 0.14))
128    }
129}
130
131fn tab_header(label: &'static str) -> impl Scene {
132    bsn! {
133        ShowcaseTab
134        Tab
135        Hovered
136        Node {
137            min_width: px(112),
138            min_height: px(36),
139            padding: UiRect::axes(px(12), px(8)),
140            border: UiRect::all(px(1)),
141            align_items: AlignItems::Center,
142            justify_content: JustifyContent::Center,
143        }
144        BackgroundColor(Color::srgb(0.13, 0.14, 0.18))
145        BorderColor::all(Color::srgb(0.24, 0.25, 0.30))
146        Children [
147            Text(label)
148            TextFont {
149                font_size: FontSize::Px(16.0)
150            }
151            TextColor(Color::srgb(0.88, 0.89, 0.92))
152        ]
153    }
154}
155
156fn selected_tab(tab: EntityTemplate) -> impl Scene {
157    let tab = OptionTemplate::Some(tab);
158    bsn! {
159        SelectedTab({tab})
160    }
161}
162
163fn section_label(label: &'static str) -> impl Scene {
164    bsn! {
165        Text(label)
166        TextFont {
167            font_size: FontSize::Px(17.0)
168        }
169        TextColor(Color::srgb(0.72, 0.76, 0.84))
170    }
171}
172
173fn controlled_selection(
174    change: On<ValueChange<Option<Entity>>>,
175    mut state: ResMut<ControlledSelection>,
176    mut commands: Commands,
177) {
178    state.0 = change.value;
179    commands.entity(change.source).insert(SelectedTab(state.0));
180}
181
182fn update_tab_styles(
183    focus: Res<InputFocus>,
184    focus_visible: Res<InputFocusVisible>,
185    mut tabs: Query<
186        (
187            Entity,
188            &Hovered,
189            Has<Selected>,
190            Has<InteractionDisabled>,
191            &mut BackgroundColor,
192            &mut BorderColor,
193        ),
194        With<ShowcaseTab>,
195    >,
196) {
197    for (entity, hovered, selected, disabled, mut background, mut border) in &mut tabs {
198        background.0 = match (disabled, selected, hovered.get()) {
199            (true, _, _) => Color::srgb(0.10, 0.10, 0.11),
200            (false, true, _) => Color::srgb(0.18, 0.34, 0.52),
201            (false, false, true) => Color::srgb(0.20, 0.21, 0.26),
202            _ => Color::srgb(0.13, 0.14, 0.18),
203        };
204        border.set_all(if focus_visible.0 && focus.get() == Some(entity) {
205            Color::srgb(0.45, 0.78, 1.0)
206        } else {
207            Color::srgb(0.24, 0.25, 0.30)
208        });
209    }
210}