From 0a11091ea86553d33520fbf74125fe63885e0842 Mon Sep 17 00:00:00 2001 From: extrawurst <776816+extrawurst@users.noreply.github.com> Date: Tue, 27 May 2025 00:27:19 +0200 Subject: [PATCH] Adding context menu example (#19245) # Objective Provides usage example of a context menu ## Testing * [x] Tested on MacOS * [x] Tested on wasm using bevy_cli --- ## Showcase https://github.com/user-attachments/assets/2e39cd32-131e-4535-beb7-b46680bca74a --------- Co-authored-by: Rob Parrett --- Cargo.toml | 11 ++ examples/README.md | 1 + examples/usages/context_menu.rs | 201 ++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 examples/usages/context_menu.rs diff --git a/Cargo.toml b/Cargo.toml index 8dd90e46b9..79967b68ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -582,6 +582,17 @@ ureq = { version = "3.0.8", features = ["json"] } wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = ["Window"] } +[[example]] +name = "context_menu" +path = "examples/usages/context_menu.rs" +doc-scrape-examples = true + +[package.metadata.example.context_menu] +name = "Context Menu" +description = "Example of a context menu" +category = "Usage" +wasm = true + [[example]] name = "hello_world" path = "examples/hello_world.rs" diff --git a/examples/README.md b/examples/README.md index 097bba2b3d..1d6377be96 100644 --- a/examples/README.md +++ b/examples/README.md @@ -579,6 +579,7 @@ Example | Description Example | Description --- | --- +[Context Menu](../examples/usages/context_menu.rs) | Example of a context menu [Cooldown](../examples/usage/cooldown.rs) | Example for cooldown on button clicks ## Window diff --git a/examples/usages/context_menu.rs b/examples/usages/context_menu.rs new file mode 100644 index 0000000000..ea595be877 --- /dev/null +++ b/examples/usages/context_menu.rs @@ -0,0 +1,201 @@ +//! This example illustrates how to create a context menu that changes the clear color + +use bevy::{ + color::palettes::basic, + ecs::{relationship::RelatedSpawner, spawn::SpawnWith}, + prelude::*, +}; +use std::fmt::Debug; + +/// event opening a new context menu at position `pos` +#[derive(Event)] +struct OpenContextMenu { + pos: Vec2, +} + +/// event will be sent to close currently open context menus +#[derive(Event)] +struct CloseContextMenus; + +/// marker component identifying root of a context menu +#[derive(Component)] +struct ContextMenu; + +/// context menu item data storing what background color `Srgba` it activates +#[derive(Component)] +struct ContextMenuItem(Srgba); + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_observer(on_trigger_menu) + .add_observer(on_trigger_close_menus) + .add_observer(text_color_on_hover::(basic::WHITE.into())) + .add_observer(text_color_on_hover::(basic::RED.into())) + .run(); +} + +/// helper function to reduce code duplication when generating almost identical observers for the hover text color change effect +fn text_color_on_hover( + color: Color, +) -> impl FnMut(Trigger>, Query<&mut TextColor>, Query<&Children>) { + move |mut trigger: Trigger>, + mut text_color: Query<&mut TextColor>, + children: Query<&Children>| { + let Ok(children) = children.get(trigger.event().target) else { + return; + }; + trigger.propagate(false); + + // find the text among children and change its color + for child in children.iter() { + if let Ok(mut col) = text_color.get_mut(child) { + col.0 = color; + } + } + } +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); + + commands.spawn(background_and_button()).observe( + // any click bubbling up here should lead to closing any open menu + |_: Trigger>, mut commands: Commands| { + commands.trigger(CloseContextMenus); + }, + ); +} + +fn on_trigger_close_menus( + _trigger: Trigger, + mut commands: Commands, + menus: Query>, +) { + for e in menus.iter() { + commands.entity(e).despawn(); + } +} + +fn on_trigger_menu(trigger: Trigger, mut commands: Commands) { + commands.trigger(CloseContextMenus); + + let pos = trigger.pos; + + debug!("open context menu at: {pos}"); + + commands + .spawn(( + Name::new("context menu"), + ContextMenu, + Node { + position_type: PositionType::Absolute, + left: Val::Px(pos.x), + top: Val::Px(pos.y), + flex_direction: FlexDirection::Column, + ..default() + }, + BorderColor::all(Color::BLACK), + BorderRadius::all(Val::Px(4.)), + BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.1)), + children![ + context_item("fuchsia", basic::FUCHSIA), + context_item("gray", basic::GRAY), + context_item("maroon", basic::MAROON), + context_item("purple", basic::PURPLE), + context_item("teal", basic::TEAL), + ], + )) + .observe( + |trigger: Trigger>, + menu_items: Query<&ContextMenuItem>, + mut clear_col: ResMut, + mut commands: Commands| { + // Note that we want to know the target of the `Pointer` event (Button) here. + // Not to be confused with the trigger `target` + let target = trigger.event().target; + + if let Ok(item) = menu_items.get(target) { + clear_col.0 = item.0.into(); + commands.trigger(CloseContextMenus); + } + }, + ); +} + +fn context_item(text: &str, col: Srgba) -> impl Bundle + use<> { + ( + Name::new(format!("item-{}", text)), + ContextMenuItem(col), + Button, + Node { + padding: UiRect::all(Val::Px(5.0)), + ..default() + }, + children![( + Pickable::IGNORE, + Text::new(text), + TextFont { + font_size: 24.0, + ..default() + }, + TextColor(Color::WHITE), + )], + ) +} + +fn background_and_button() -> impl Bundle + use<> { + ( + Name::new("background"), + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + ZIndex(-10), + Children::spawn(SpawnWith(|parent: &mut RelatedSpawner| { + parent + .spawn(( + Name::new("button"), + Button, + Node { + width: Val::Px(250.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BorderColor::all(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(Color::BLACK), + children![( + Pickable::IGNORE, + Text::new("Context Menu"), + TextFont { + font_size: 28.0, + ..default() + }, + TextColor(Color::WHITE), + TextShadow::default(), + )], + )) + .observe( + |mut trigger: Trigger>, mut commands: Commands| { + // by default this event would bubble up further leading to the `CloseContextMenus` + // event being triggered and undoing the opening of one here right away. + trigger.propagate(false); + + debug!("click: {}", trigger.pointer_location.position); + + commands.trigger(OpenContextMenu { + pos: trigger.pointer_location.position, + }); + }, + ); + })), + ) +}