haalka 0.7.1

ergonomic reactive Bevy UI library powered by FRP signals
Documentation
//! Dragging with z-index stacking

mod utils;
use utils::*;

use bevy::{prelude::*, ui::Pressed};
use bevy_rand::prelude::*;
use haalka::prelude::*;

fn main() {
    App::new()
        .add_plugins((examples_plugin, EntropyPlugin::<WyRand>::default()))
        .add_systems(
            Startup,
            (
                |world: &mut World| {
                    ui_root().spawn(world);
                },
                camera,
            ),
        )
        .insert_resource(MaxZIndex::default())
        .run();
}

#[derive(Resource, Default)]
struct MaxZIndex(i32);

#[derive(Component, Default)]
struct DragOffset(Vec2);

const WIDTH: f32 = 100.0;
const HEIGHT: f32 = 100.0;

fn camera(mut commands: Commands) {
    commands.spawn(Camera2d);
}

fn calculate_and_set_drag_offset(
    entity: Entity,
    press_position: Vec2,
    nodes: &Query<&Node>,
    mut drag_offsets: Query<&mut DragOffset>,
) {
    if let Ok(node) = nodes.get(entity) {
        let left = match node.left {
            Val::Px(px) => px,
            _ => 0.0,
        };
        let top = match node.top {
            Val::Px(px) => px,
            _ => 0.0,
        };
        let offset = Vec2::new(press_position.x - left, press_position.y - top);
        if let Ok(mut drag_offset) = drag_offsets.get_mut(entity) {
            drag_offset.0 = offset;
        }
    }
}

fn update_z_index(entity: Entity, mut max_z_index: ResMut<MaxZIndex>, mut z_indices: Query<&mut GlobalZIndex>) {
    max_z_index.0 += 1;
    if let Ok(mut z_index) = z_indices.get_mut(entity) {
        z_index.0 = max_z_index.0;
    }
}

fn ui_root() -> impl Element {
    let any_pressed = signal::from_system(|In(_), presseds: Query<&Pressed>| !presseds.is_empty()).dedupe();
    let any_dragged = signal::from_system(|In(_), draggeds: Query<&Dragged>| !draggeds.is_empty()).dedupe();
    El::<Node>::new()
        .with_node(|mut node| {
            node.width = Val::Percent(100.0);
            node.height = Val::Percent(100.0);
        })
        .insert(Pickable::default())
        .cursor_disableable_signal(
            CursorIcon::System(SystemCursorIcon::Default),
            signal::any!(any_pressed, any_dragged).dedupe(),
        )
        .align_content(Align::center())
        .child(Row::<Node>::new().items((0..=5).map(square)))
}

fn square(i: usize) -> impl Element {
    let lazy_entity = LazyEntity::new();

    El::<Node>::new()
        .with_node(move |mut node| {
            node.width = Val::Px(WIDTH);
            node.height = Val::Px(HEIGHT);
        })
        .align_content(Align::center())
        .with_builder(|builder| {
            builder.on_spawn_with_system(
                |In(entity): In<Entity>,
                 mut rng: Single<&mut WyRand, With<GlobalRng>>,
                 mut backgrounds: Query<&mut BackgroundColor>| {
                    if let Ok(mut bg) = backgrounds.get_mut(entity) {
                        *bg = BackgroundColor(random_color(rng.as_mut()));
                    }
                },
            )
        })
        .global_z_index(GlobalZIndex(1))
        .cursor_signal({
            let pressed = signal::from_entity(lazy_entity.clone())
                .has_component::<Pressed>()
                .dedupe();
            let dragged = signal::from_entity(lazy_entity.clone())
                .has_component::<Dragged>()
                .dedupe();
            let hovered = signal::from_entity(lazy_entity.clone())
                .has_component::<Hovered>()
                .dedupe();
            signal::zip!(signal::any!(pressed, dragged).dedupe(), hovered)
                .dedupe()
                .map_in(|(dragged, hovered)| {
                    if dragged {
                        SystemCursorIcon::Grabbing
                    } else if hovered {
                        SystemCursorIcon::Grab
                    } else {
                        SystemCursorIcon::Default
                    }
                })
                .map_in(CursorIcon::System)
                .dedupe()
        })
        .lazy_entity(lazy_entity.clone())
        .insert((Pickable::default(), Hoverable, Pressable, DragOffset::default()))
        .observe(
            |click: On<Pointer<Press>>,
             max_z_index: ResMut<MaxZIndex>,
             z_indices: Query<&mut GlobalZIndex>,
             drag_offsets: Query<&mut DragOffset>,
             nodes: Query<&Node>| {
                calculate_and_set_drag_offset(click.entity, click.pointer_location.position, &nodes, drag_offsets);
                update_z_index(click.entity, max_z_index, z_indices);
            },
        )
        .on_dragged(
            |In((entity, drag_data)): In<(Entity, DragData)>, mut nodes: Query<(&mut Node, &DragOffset)>| {
                if drag_data.dragged
                    && let Ok((node, drag_offset)) = nodes.get_mut(entity)
                {
                    set_dragging_position(node, drag_data.pointer_location.position, drag_offset.0);
                }
            },
        )
        .child(El::<Text>::new().text(Text::new(i.to_string())))
}

fn set_dragging_position(mut node: Mut<Node>, pointer_position: Vec2, offset: Vec2) {
    node.left = Val::Px(pointer_position.x - offset.x);
    node.top = Val::Px(pointer_position.y - offset.y);
}