UI (User Interface) / Headless Tabs

Back to examples View in GitHub

Demonstrates the behavior-only tab widgets in bevy_ui_widgets.

use bevy::{
    ecs::template::{EntityTemplate, OptionTemplate},
    input_focus::{
        tab_navigation::{TabGroup, TabNavigationPlugin},
        InputFocus, InputFocusVisible,
    },
    picking::hover::Hovered,
    prelude::*,
    ui::{InteractionDisabled, Selected},
    ui_widgets::{
        tablist_self_update, ControlOrientation, SelectedTab, Tab, TabActivation, TabList,
        ValueChange,
    },
};

#[derive(Component, Default, Clone)]
struct ShowcaseTab;

#[derive(Resource, Default)]
struct ControlledSelection(Option<Entity>);

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, TabNavigationPlugin))
        .init_resource::<ControlledSelection>()
        .add_systems(Startup, showcase.spawn())
        .add_systems(Update, update_tab_styles)
        .run();
}

fn showcase() -> impl SceneList {
    bsn_list! {
        Camera2d
        --
        Node {
            width: percent(100),
            height: percent(100),
            padding: UiRect::all(px(24)),
            display: Display::Flex,
            flex_direction: FlexDirection::Column,
            align_items: AlignItems::Start,
            row_gap: px(18),
            overflow: Overflow::scroll_y(),
        }
        BackgroundColor(Color::srgb(0.06, 0.07, 0.09))
        TabGroup
        Children [
            @section_label("Horizontal automatic - self-updating")
            --
            #automatic
            @tab_strip(ControlOrientation::Horizontal)
            TabList {
                orientation: ControlOrientation::Horizontal,
                activation: TabActivation::Automatic,
            }
            @selected_tab(#automatic_general)
            on(tablist_self_update)
            Children [
                #automatic_general
                @tab_header("General")
                --
                @tab_header("Rendering")
                --
                @tab_header("Disabled")
                InteractionDisabled
            ]
            --
            @section_label("Horizontal manual - focus and selection are separate")
            --
            @tab_strip(ControlOrientation::Horizontal)
            TabList
            @selected_tab(#manual_scene)
            on(tablist_self_update)
            Children [
                #manual_scene @tab_header("Scene")
                --
                @tab_header("Assets")
                --
                @tab_header("Inspector")
            ]
            --
            @section_label("Vertical manual")
            --
            @tab_strip(ControlOrientation::Vertical)
            TabList {
                orientation: ControlOrientation::Vertical,
                activation: TabActivation::Manual,
            }
            @selected_tab(#vertical_transform)
            on(tablist_self_update)
            Children [
                #vertical_transform @tab_header("Transform")
                --
                @tab_header("Visibility")
                --
                @tab_header("Metadata")
            ]
            --
            @section_label("Controlled - observer updates external state")
            --
            @tab_strip(ControlOrientation::Horizontal)
            TabList
            @selected_tab(#controlled_a)
            on(controlled_selection)
            Children [
                #controlled_a @tab_header("External A")
                --
                @tab_header("External B")
            ]
        ]
    }
}

fn tab_strip(orientation: ControlOrientation) -> impl Scene {
    let flex_direction = match orientation {
        ControlOrientation::Horizontal => FlexDirection::Row,
        ControlOrientation::Vertical => FlexDirection::Column,
    };
    bsn! {
        Node {
            display: Display::Flex,
            flex_direction,
            align_items: AlignItems::Stretch,
        }
        BackgroundColor(Color::srgb(0.10, 0.11, 0.14))
    }
}

fn tab_header(label: &'static str) -> impl Scene {
    bsn! {
        ShowcaseTab
        Tab
        Hovered
        Node {
            min_width: px(112),
            min_height: px(36),
            padding: UiRect::axes(px(12), px(8)),
            border: UiRect::all(px(1)),
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Center,
        }
        BackgroundColor(Color::srgb(0.13, 0.14, 0.18))
        BorderColor::all(Color::srgb(0.24, 0.25, 0.30))
        Children [
            Text(label)
            TextFont {
                font_size: FontSize::Px(16.0)
            }
            TextColor(Color::srgb(0.88, 0.89, 0.92))
        ]
    }
}

fn selected_tab(tab: EntityTemplate) -> impl Scene {
    let tab = OptionTemplate::Some(tab);
    bsn! {
        SelectedTab({tab})
    }
}

fn section_label(label: &'static str) -> impl Scene {
    bsn! {
        Text(label)
        TextFont {
            font_size: FontSize::Px(17.0)
        }
        TextColor(Color::srgb(0.72, 0.76, 0.84))
    }
}

fn controlled_selection(
    change: On<ValueChange<Option<Entity>>>,
    mut state: ResMut<ControlledSelection>,
    mut commands: Commands,
) {
    state.0 = change.value;
    commands.entity(change.source).insert(SelectedTab(state.0));
}

fn update_tab_styles(
    focus: Res<InputFocus>,
    focus_visible: Res<InputFocusVisible>,
    mut tabs: Query<
        (
            Entity,
            &Hovered,
            Has<Selected>,
            Has<InteractionDisabled>,
            &mut BackgroundColor,
            &mut BorderColor,
        ),
        With<ShowcaseTab>,
    >,
) {
    for (entity, hovered, selected, disabled, mut background, mut border) in &mut tabs {
        background.0 = match (disabled, selected, hovered.get()) {
            (true, _, _) => Color::srgb(0.10, 0.10, 0.11),
            (false, true, _) => Color::srgb(0.18, 0.34, 0.52),
            (false, false, true) => Color::srgb(0.20, 0.21, 0.26),
            _ => Color::srgb(0.13, 0.14, 0.18),
        };
        border.set_all(if focus_visible.0 && focus.get() == Some(entity) {
            Color::srgb(0.45, 0.78, 1.0)
        } else {
            Color::srgb(0.24, 0.25, 0.30)
        });
    }
}