bevy_lunex 0.2.4

Blazingly fast path based retained layout engine for Bevy entities, built around vanilla Bevy ECS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
use crate::*;
use bevy::{input::{gamepad::GamepadButtonChangedEvent, mouse::MouseButtonInput, ButtonState}, render::camera::RenderTarget, utils::HashMap, window::{CursorGrabMode, PrimaryWindow, WindowRef}};
use picking_core::PickSet;
use pointer::{InputMove, InputPress, Location};

// #===================#
// #=== CURSOR TYPE ===#

/// Component for easy cursor control.
/// Read more about it in the [docs](https://bytestring-net.github.io/bevy_lunex/advanced/3_cursor.html)
#[derive(Component, Debug, Clone, PartialEq)]
pub struct Cursor2d {
    /// Indicates which cursor is being requested.
    cursor_request: CursorIcon,
    /// Indicates the priority of the requested cursor.
    cursor_request_priority: f32,
    /// Map which cursor has which atlas index and offset
    cursor_atlas_map: HashMap<CursorIcon, (usize, Vec2)>,
    /// Location of the cursor (same as [`Transform`] without sprite offset).
    pub location: Vec2,
    /// If the cursor is allowed to leave window. Does nothing is cursor is controlled by gamepad.
    pub confined: bool,
    /// A toggle if the cursor should be visible
    pub visible: bool,
}
impl Cursor2d {
    /// Creates new default Cursor2d.
    pub fn new() -> Cursor2d {
        Cursor2d {
            cursor_request: CursorIcon::Default,
            cursor_request_priority: 0.0,
            cursor_atlas_map: HashMap::new(),
            location: Vec2::ZERO,
            confined: false,
            visible: true,
        }
    }
    /// A method to request a new cursor icon. Works only if priority is higher than already set priority this tick.
    pub fn request_cursor(&mut self, request: CursorIcon, priority: f32) {
        if priority > self.cursor_request_priority {
            self.cursor_request = request;
            self.cursor_request_priority = priority;
        }
    }
    /// This function binds the specific cursor icon to an image index that is used if the entity has texture atlas attached to it.
    pub fn set_index(mut self, icon: CursorIcon, index: usize, offset: impl Into<Vec2>) -> Self {
        self.cursor_atlas_map.insert(icon, (index, offset.into()));
        self
    }
}
impl Default for Cursor2d {
    fn default() -> Self {
        Self {
            cursor_request: Default::default(),
            cursor_request_priority: Default::default(),
            cursor_atlas_map: Default::default(),
            location: Default::default(),
            confined: Default::default(),
            visible: true,
        }
    }
}

/// This will make the [`Cursor2d`] controllable by specific gamepad.
#[derive(Component, Debug, Clone, PartialEq)]
pub struct GamepadCursor {
    /// Gamepad index
    pub id: usize,
    /// This struct defines how should the cursor movement behave.
    pub mode: GamepadCursorMode,
    /// Cursor speed scale
    pub speed: f32,
}
impl GamepadCursor {
    /// Creates a new instance from gamepad id.
    pub fn new(id: usize) -> Self {
        Self { id, ..Default::default() }
    }
}
impl Default for GamepadCursor {
    fn default() -> Self {
        Self { id: 0, mode: Default::default(), speed: 1.0 }
    }
}


/// This struct defines how should the cursor movement behave.
#[derive(Debug, Clone, Default, PartialEq)]
pub enum GamepadCursorMode {
    /// Cursor will freely move on input.
    #[default]
    Free,
    // /// Will try to snap to nearby nodes on input.
    //Snap,
}


// #======================#
// #=== CURSOR BUNDLES ===#

/// Use this bundle to spawn native cursor
#[derive(Bundle)]
pub struct CursorBundle {
    /// Main cursor component
    pub cursor: Cursor2d,
    /// The virtual pointer that the cursor controls
    pub pointer: PointerBundle,
}
impl Default for CursorBundle {
    fn default() -> Self {
        Self {
            cursor: default(),
            pointer: PointerBundle::new(PointerId::Custom(pointer::Uuid::new_v4())),
        }
    }
}

/// Use this bundle to spawn styled custom cursor
#[derive(Bundle)]
pub struct StyledCursorBundle {
    /// Main cursor component
    pub cursor: Cursor2d,
    /// The virtual pointer that the cursor controls
    pub pointer: PointerBundle,
    /// Sprite atlas for the cursor
    pub atlas: TextureAtlas,
    /// Sprite cursor
    pub sprite: SpriteBundle,
    /// Required to be [`Pickable::IGNORE`]
    pub pickable: Pickable,
}
impl Default for StyledCursorBundle {
    fn default() -> Self {
        Self {
            cursor: default(),
            pointer: PointerBundle::new(PointerId::Custom(pointer::Uuid::new_v4())),
            atlas: default(),
            sprite: default(),
            pickable: Pickable::IGNORE,
        }
    }
}


// #========================#
// #=== CURSOR FUNCTIONS ===#

/// This function controls the visibility of the cursor
fn cursor_set_visibility(
    mut windows: Query<&mut Window, With<PrimaryWindow>>,
    mut query: Query<(&Cursor2d, Option<&mut Visibility>, Has<GamepadCursor>, Has<Handle<Image>>)>
) {
    if let Ok(mut window) = windows.get_single_mut() {
        for (cursor, optional_visibility, has_gamepad, has_image) in &mut query {
            // If we have visibility then change it
            if let Some(mut visibility) = optional_visibility {
                *visibility = if cursor.visible { Visibility::Visible } else { Visibility::Hidden };
                if window.cursor_position().is_none() && !has_gamepad { *visibility = Visibility::Hidden }
            }

            // If it is not a gamepad
            if !has_gamepad {
                // Set native cursor to invisible if image is attached to the cursor
                window.cursor.visible = if has_image { false } else { cursor.visible };
            }
        }
    }
}

/// This function controls the native mouse cursor settings
fn cursor_change_native(
    mut windows: Query<&mut Window, With<PrimaryWindow>>,
    mut query: Query<&Cursor2d, Without<GamepadCursor>>
) {
    if let Ok(mut window) = windows.get_single_mut() {
        for cursor in &mut query {
            // Change native cursor
            if window.cursor.visible { window.cursor.icon = cursor.cursor_request; }

            // Change grab mode
            window.cursor.grab_mode = if cursor.confined { CursorGrabMode::Confined } else { CursorGrabMode::None }
        }
    }
}


/// This function controls the location of the cursor based on gamepad input
fn gamepad_move_cursor(
    axis: Res<Axis<GamepadAxis>>,
    time: Res<Time>,
    windows: Query<&Window, With<PrimaryWindow>>,
    mut query: Query<(&mut Cursor2d, &GamepadCursor)>,
) {
    if let Ok(window) = windows.get_single() {
        for (mut cursor, gamepad) in query.iter_mut() {
            // Pull axis values
            let x = axis.get(GamepadAxis { gamepad: Gamepad::new(gamepad.id), axis_type: GamepadAxisType::LeftStickX });
            let y = axis.get(GamepadAxis { gamepad: Gamepad::new(gamepad.id), axis_type: GamepadAxisType::LeftStickY });

            if let (Some(x), Some(y)) = (x, y) {
                // Move the cursor
                cursor.location.x += x * time.delta_seconds() * 500.0 * gamepad.speed;
                cursor.location.y += y * time.delta_seconds() * 500.0 * gamepad.speed;

                // Clamp the cursor within window
                let w = window.width()/2.0;
                let h = window.height()/2.0;
                cursor.location.x = cursor.location.x.clamp(-w, w);
                cursor.location.y = cursor.location.y.clamp(-h, h);
            }
        }
    }
}

/// This function controls the location of the cursor based on mouse input
fn mouse_move_cursor(
    windows: Query<&Window, With<PrimaryWindow>>,
    cameras: Query<&OrthographicProjection>,
    mut query: Query<(&mut Cursor2d, Option<&Parent>), Without<GamepadCursor>>
) {
    if let Ok(window) = windows.get_single() {
        for (mut cursor, parent_option) in &mut query {
            if let Some(position) = window.cursor_position() {
                // Get projection scale to account for zoomed cameras
                let scale = if let Some(parent) = parent_option {
                    if let Ok(projection) = cameras.get(**parent) { projection.scale } else { 1.0 }
                } else { 1.0 };
                

                // Move the cursor
                cursor.location.x = (position.x - window.width()*0.5) * scale;
                cursor.location.y = -((position.y - window.height()*0.5) * scale);
            }
        }
    }
}

/// This function updates the transform component with the modified location and sprite offsets
fn cursor_update_transform(
    mut query: Query<(&Cursor2d, &mut Transform)>
) {
    for (cursor, mut transform) in &mut query {
        let sprite_offset = cursor.cursor_atlas_map.get(&cursor.cursor_request).unwrap_or(&(0, Vec2::ZERO)).1;
        transform.translation.x = cursor.location.x - sprite_offset.x * transform.scale.x;
        transform.translation.y = cursor.location.y + sprite_offset.y * transform.scale.y;
    }
}

/// This function controls virtual pointer attached to the cursor
fn cursor_move_virtual_pointer(
    windows: Query<(Entity, &Window), With<PrimaryWindow>>,
    mut query: Query<(&mut PointerLocation, &Cursor2d)>,
) {
    if let Ok((win_entity, window)) = windows.get_single() {
        for (mut pointer, cursor) in query.iter_mut() {
            // Change the pointer location
            pointer.location = Some(pointer::Location {
                target: RenderTarget::Window(WindowRef::Primary).normalize(Some(win_entity)).unwrap(),
                position: Vec2 {
                    x: cursor.location.x + window.width()/2.0,
                    y: -cursor.location.y + window.height()/2.0,
                }.round(),
            });
        }
    }
}

/// This function sends mouse pointer events to be processed by the mod picking core plugin
fn cursor_mouse_pick_events(
    // Input
    mut mouse_inputs: EventReader<MouseButtonInput>,
    mut cursor_last: Local<HashMap<PointerId, Vec2>>,
    pointers: Query<(&PointerId, &PointerLocation), (With<Cursor2d>, Without<GamepadCursor>)>,
    // Output
    mut pointer_move: EventWriter<InputMove>,
    mut pointer_presses: EventWriter<InputPress>,
) {
    // Send mouse movement events
    for (pointer, location) in &pointers {
        if let Some(location) = &location.location {
            let last = cursor_last.get(pointer).unwrap_or(&Vec2::ZERO);
            if *last == location.position { continue; }

            pointer_move.send(InputMove::new(
                *pointer,
                Location {
                    target: location.target.clone(),
                    position: location.position,
                },
                location.position - *last,
            ));
            cursor_last.insert(*pointer, location.position);
        }
    }

    // Send mouse click events
    for input in mouse_inputs.read() {
        let button = match input.button {
            MouseButton::Left => PointerButton::Primary,
            MouseButton::Right => PointerButton::Secondary,
            MouseButton::Middle => PointerButton::Middle,
            MouseButton::Other(_) => continue,
            MouseButton::Back => continue,
            MouseButton::Forward => continue,
        };

        match input.state {
            ButtonState::Pressed => {
                for (pointer, _) in &pointers {
                    pointer_presses.send(InputPress::new_down(*pointer, button));
                }
            }
            ButtonState::Released => {
                for (pointer, _) in &pointers {
                    pointer_presses.send(InputPress::new_up(*pointer, button));
                }
            }
        }
    }
}

/// This function sends mouse pointer events to be processed by the mod picking core plugin
fn cursor_gamepad_pick_events(
    // Input
    mut gamepad_inputs: EventReader<GamepadButtonChangedEvent>,
    mut cursor_last: Local<HashMap<PointerId, Vec2>>,
    pointers: Query<(&PointerId, &PointerLocation, &GamepadCursor), With<Cursor2d>>,
    // Output
    mut pointer_move: EventWriter<InputMove>,
    mut pointer_presses: EventWriter<InputPress>,
) {
    // Send mouse movement events
    for (pointer, location, _) in &pointers {
        if let Some(location) = &location.location {
            let last = cursor_last.get(pointer).unwrap_or(&Vec2::ZERO);
            if *last == location.position { continue; }

            pointer_move.send(InputMove::new(
                *pointer,
                Location {
                    target: location.target.clone(),
                    position: location.position,
                },
                location.position - *last,
            ));
            cursor_last.insert(*pointer, location.position);
        }
    }

    // Send mouse click events
    for input in gamepad_inputs.read() {
        let button = match input.button_type {
            GamepadButtonType::South => PointerButton::Primary,
            GamepadButtonType::East => PointerButton::Secondary,
            GamepadButtonType::West => PointerButton::Middle,
            _ => continue,
        };

        match input.value {
            1.0 => {
                for (pointer, _, gamepad) in &pointers {
                    if gamepad.id != input.gamepad.id { continue; }
                    pointer_presses.send(InputPress::new_down(*pointer, button));
                }
            }
            0.0 => {
                for (pointer, _, gamepad) in &pointers {
                    if gamepad.id != input.gamepad.id { continue; }
                    pointer_presses.send(InputPress::new_up(*pointer, button));
                }
            },
            _ => {}
        }
    }
}


/// This function resets the requested cursor back to default every tick
fn cursor_reset_icon(
    mut query: Query<&mut Cursor2d>
) {
    for mut cursor in &mut query {
        cursor.cursor_request = CursorIcon::Default;
        cursor.cursor_request_priority = 0.0;
    }
}

/// This function updates the atlas index texture based on requested cursor icon
fn cursor_update_texture(
    mut query: Query<(&Cursor2d, &mut TextureAtlas)>
) {
    for (cursor, mut atlas) in &mut query {
        atlas.index = cursor.cursor_atlas_map.get(&cursor.cursor_request).unwrap_or(&(0, Vec2::ZERO)).0;
    }
}


/// Requests cursor icon on hover
#[derive(Component, Debug, Clone, PartialEq)]
pub struct OnHoverSetCursor {
    /// Cursor type to request on hover
    pub cursor: CursorIcon,
}
impl OnHoverSetCursor {
    /// Creates new struct
    pub fn new(cursor: CursorIcon) -> Self {
        OnHoverSetCursor {
            cursor
        }
    }
}
fn on_hover_set_cursor(query: Query<(&UiAnimator<Hover>, &OnHoverSetCursor)>, mut cursor: Query<&mut Cursor2d>) {
    for (control, hover_cursor) in &query {
        if control.is_forward() {
            if let Ok(mut cursor) = cursor.get_single_mut(){
                cursor.request_cursor(hover_cursor.cursor, 1.0);
            }
        }
    }
}


// #==============#
// #=== PLUGIN ===#

pub struct CursorPlugin;
impl Plugin for CursorPlugin {
    fn build(&self, app: &mut App) {
        app

            // Add systems for mod picking event emitters
            .add_systems(First, (cursor_mouse_pick_events, cursor_gamepad_pick_events, apply_deferred).chain().in_set(PickSet::Input))

            // Add core systems 
            .add_systems(PreUpdate, cursor_reset_icon)
            .add_systems(PreUpdate, (gamepad_move_cursor, mouse_move_cursor, cursor_update_transform, cursor_move_virtual_pointer).chain())
            .add_systems(PostUpdate, cursor_set_visibility)
            .add_systems(PostUpdate, cursor_change_native)
            .add_systems(PostUpdate, cursor_update_texture)

            // Other stuff
            .add_systems(Update, on_hover_set_cursor);
    }
}