//! Demonstrates why pointer capture matters during drag operations. //! //! A slider sits above a row of buttons. Without [`PointerCaptureMap`], dragging the slider thumb //! over the buttons causes them to show a hover highlight — misleading the user into thinking the //! button is about to be activated. With capture enabled the slider owns the pointer for the //! duration of the drag, so no other widget receives hover state. //! //! Toggle capture on/off with the button at the bottom to see the difference. //! //! Additional UI elements showcased: //! - A **fill bar** that grows with the slider value. //! - **Min / max labels** flanking the track. //! - A **Reset button** that snaps the value back to the default. use bevy::prelude::*; const TRACK_WIDTH: f32 = 500.0; const TRACK_HEIGHT: f32 = 14.0; const THUMB_SIZE: f32 = 28.0; const THUMB_HALF: f32 = THUMB_SIZE / 2.0; const FILL_HEIGHT: f32 = 8.0; const SLIDER_DEFAULT_VALUE: f32 = 0.5; const COLOR_TRACK: Color = Color::srgb(0.2, 0.2, 0.2); const COLOR_FILL: Color = Color::srgb(0.35, 0.65, 0.35); const COLOR_FILL_DRAGGING: Color = Color::srgb(0.45, 0.85, 0.45); const COLOR_THUMB_IDLE: Color = Color::srgb(0.85, 0.55, 0.1); const COLOR_THUMB_HOVER: Color = Color::srgb(1.0, 0.75, 0.2); const COLOR_DECOY_IDLE: Color = Color::srgb(0.2, 0.45, 0.8); const COLOR_DECOY_HOVER: Color = Color::srgb(0.9, 0.2, 0.2); const COLOR_TOGGLE_ON: Color = Color::srgb(0.15, 0.65, 0.3); const COLOR_TOGGLE_OFF: Color = Color::srgb(0.55, 0.15, 0.15); const COLOR_RESET_IDLE: Color = Color::srgb(0.3, 0.3, 0.55); const COLOR_RESET_HOVER: Color = Color::srgb(0.5, 0.5, 0.75); /// The normalized (0 – 1) position of the slider thumb. #[derive(Resource)] struct SliderValue(f32); impl Default for SliderValue { fn default() -> Self { Self(SLIDER_DEFAULT_VALUE) } } /// Whether [`PointerCaptureMap`] is used while dragging the thumb. #[derive(Resource)] struct CaptureEnabled(bool); impl Default for CaptureEnabled { fn default() -> Self { Self(true) } } #[derive(Component)] struct SliderThumb; #[derive(Component)] struct SliderFill; #[derive(Component)] struct SliderLabel; #[derive(Component)] struct ToggleLabel; #[derive(Component)] struct ToggleButton; #[derive(Component)] struct ResetButton; #[derive(Component)] struct DecoyButton; /// X offset of the left edge of the thumb node. fn thumb_left(v: f32) -> f32 { v.clamp(0.0, 1.0) * (TRACK_WIDTH - THUMB_SIZE) } /// Width of the fill bar (reaches to the center of the thumb). fn fill_width(v: f32) -> f32 { v.clamp(0.0, 1.0) * (TRACK_WIDTH - THUMB_SIZE) + THUMB_HALF } fn main() { App::new() .add_plugins(DefaultPlugins) .init_resource::() .init_resource::() .add_systems(Startup, setup) .add_systems(Update, (sync_thumb_and_label, sync_fill)) .run(); } fn setup(mut commands: Commands) { commands.spawn(Camera2d); // Root column – centered, full-screen, non-pickable container. commands .spawn(( Node { width: Val::Percent(100.0), height: Val::Percent(100.0), flex_direction: FlexDirection::Column, align_items: AlignItems::Center, justify_content: JustifyContent::Center, row_gap: Val::Px(28.0), ..default() }, Pickable::IGNORE, )) .with_children(|root| { root.spawn(( Text::new( "Drag the slider over the blue buttons.\n\ With capture ON they stay blue. With capture OFF they incorrectly turn red.", ), TextFont::from_font_size(15.0), TextColor(Color::srgb(0.75, 0.75, 0.75)), )); root.spawn(( Node { flex_direction: FlexDirection::Row, align_items: AlignItems::Center, column_gap: Val::Px(10.0), ..default() }, Pickable::IGNORE, )) .with_children(|row| { row.spawn(( Text::new("0.0"), TextFont::from_font_size(13.0), TextColor(Color::srgb(0.55, 0.55, 0.55)), )); row.spawn(( Node { width: Val::Px(TRACK_WIDTH), height: Val::Px(TRACK_HEIGHT), position_type: PositionType::Relative, align_items: AlignItems::Center, ..default() }, BackgroundColor(COLOR_TRACK), Pickable::IGNORE, )) .with_children(|track| { track.spawn(( SliderFill, Node { width: Val::Px(fill_width(SLIDER_DEFAULT_VALUE)), height: Val::Px(FILL_HEIGHT), position_type: PositionType::Absolute, left: Val::Px(0.0), ..default() }, BackgroundColor(COLOR_FILL), Pickable::IGNORE, )); track .spawn(( SliderThumb, Node { width: Val::Px(THUMB_SIZE), height: Val::Px(THUMB_SIZE), position_type: PositionType::Absolute, left: Val::Px(thumb_left(SLIDER_DEFAULT_VALUE)), border_radius: BorderRadius::all(Val::Px(4.0)), ..default() }, BackgroundColor(COLOR_THUMB_IDLE), )) .observe(on_drag_start) .observe(on_drag) .observe(on_drag_end) .observe( |_: On, mut q: Single<&mut BackgroundColor, With>| { q.0 = COLOR_THUMB_HOVER; }, ) .observe( |_: On, mut q: Single<&mut BackgroundColor, With>| { q.0 = COLOR_THUMB_IDLE; }, ); }); row.spawn(( Text::new("1.0"), TextFont::from_font_size(13.0), TextColor(Color::srgb(0.55, 0.55, 0.55)), )); }); root.spawn(( SliderLabel, Text::new(format!("Value: {:.2}", SLIDER_DEFAULT_VALUE)), TextFont::from_font_size(18.0), )); root.spawn(( Node { flex_direction: FlexDirection::Row, column_gap: Val::Px(16.0), ..default() }, Pickable::IGNORE, )) .with_children(|row| { for label in ["Button A", "Button B", "Button C", "Button D", "Button E"] { spawn_decoy_button(row, label); } }); root.spawn(( Node { flex_direction: FlexDirection::Row, column_gap: Val::Px(20.0), align_items: AlignItems::Center, ..default() }, Pickable::IGNORE, )) .with_children(|row| { row.spawn(( ToggleButton, Node { padding: UiRect::axes(Val::Px(20.0), Val::Px(10.0)), border_radius: BorderRadius::all(Val::Px(6.0)), ..default() }, BackgroundColor(COLOR_TOGGLE_ON), )) .with_children(|btn| { btn.spawn(( ToggleLabel, Text::new("Capture: ON (click to toggle)"), TextFont::from_font_size(16.0), TextColor(Color::WHITE), Pickable::IGNORE, )); }) .observe(on_toggle_click); row.spawn(( ResetButton, Node { padding: UiRect::axes(Val::Px(20.0), Val::Px(10.0)), border_radius: BorderRadius::all(Val::Px(6.0)), ..default() }, BackgroundColor(COLOR_RESET_IDLE), )) .with_children(|btn| { btn.spawn(( Text::new("Reset"), TextFont::from_font_size(16.0), TextColor(Color::WHITE), Pickable::IGNORE, )); }) .observe( |_: On, mut q: Single<&mut BackgroundColor, With>| { q.0 = COLOR_RESET_HOVER; }, ) .observe( |_: On, mut q: Single<&mut BackgroundColor, With>| { q.0 = COLOR_RESET_IDLE; }, ) .observe(on_reset_click); }); }); } fn spawn_decoy_button(parent: &mut ChildSpawnerCommands, label: &str) { parent .spawn(( DecoyButton, Node { padding: UiRect::axes(Val::Px(24.0), Val::Px(14.0)), border_radius: BorderRadius::all(Val::Px(6.0)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, BackgroundColor(COLOR_DECOY_IDLE), )) .with_child(( Text::new(label), TextFont::from_font_size(15.0), TextColor(Color::WHITE), Pickable::IGNORE, )) .observe( |event: On, mut colors: Query<&mut BackgroundColor, With>| { if let Ok(mut color) = colors.get_mut(event.entity) { color.0 = COLOR_DECOY_HOVER; } }, ) .observe( |event: On, mut colors: Query<&mut BackgroundColor, With>| { if let Ok(mut color) = colors.get_mut(event.entity) { color.0 = COLOR_DECOY_IDLE; } }, ); } fn on_drag_start( event: On, capture_enabled: Res, mut capture_map: ResMut, mut fill_color: Single<&mut BackgroundColor, With>, ) { if capture_enabled.0 { capture_map.capture(event.pointer.id, event.entity, event.hit.clone()); } // Signal active drag with a brighter fill color. fill_color.0 = COLOR_FILL_DRAGGING; } fn on_drag(event: On, mut value: ResMut) { value.0 = (value.0 + event.delta.x / (TRACK_WIDTH - THUMB_SIZE)).clamp(0.0, 1.0); } fn on_drag_end( event: On, mut capture_map: ResMut, mut fill_color: Single<&mut BackgroundColor, With>, ) { capture_map.release(event.pointer.id); fill_color.0 = COLOR_FILL; } fn on_toggle_click( _: On, mut capture_enabled: ResMut, mut label: Single<&mut Text, With>, mut toggle_bg: Single<&mut BackgroundColor, With>, ) { capture_enabled.0 = !capture_enabled.0; if capture_enabled.0 { label.0 = "Capture: ON (click to toggle)".into(); toggle_bg.0 = COLOR_TOGGLE_ON; } else { label.0 = "Capture: OFF (click to toggle)".into(); toggle_bg.0 = COLOR_TOGGLE_OFF; } } fn on_reset_click(_: On, mut value: ResMut) { value.0 = SLIDER_DEFAULT_VALUE; } /// Moves the thumb and updates the numeric readout. fn sync_thumb_and_label( value: Res, mut thumb: Single<&mut Node, With>, mut value_label: Single<&mut Text, With>, ) { thumb.left = Val::Px(thumb_left(value.0)); value_label.0 = format!("Value: {:.2}", value.0); } /// Resizes the fill bar to match the current slider value. fn sync_fill(value: Res, mut fill: Single<&mut Node, With>) { fill.width = Val::Px(fill_width(value.0)); }