inventory/
inventory.rs

1//! - Fixed-size grid, some spaces with items and some empty.
2//! - Each item slot has an image of the item and the item count overlayed on the image.
3//! - Items can be moved with drag and drop.
4//!   - Both image and item count move along with the cursor while dragging.
5//!   - The image and item count are not visible in the original position while dragging.
6//!   - You can leave the bounding box of the inventory while dragging.
7//! - A tooltip with the item's name is shown when hovering over an item.
8
9// TODO: fix cursor not updating when placing an item in an empty cell and then moving cursor
10// outside
11
12mod utils;
13use utils::*;
14
15use std::{collections::HashMap, sync::OnceLock};
16
17use bevy::prelude::*;
18use bevy_asset_loader::prelude::*;
19use haalka::{prelude::*, raw::DeferredUpdaterAppendDirection};
20use rand::{
21    Rng,
22    distr::{Bernoulli, Distribution},
23};
24
25fn main() {
26    App::new()
27        .add_plugins(examples_plugin)
28        .init_state::<AssetState>()
29        .add_loading_state(
30            LoadingState::new(AssetState::Loading)
31                .continue_to_state(AssetState::Loaded)
32                .load_collection::<RpgIconSheet>(),
33        )
34        // .add_systems(Startup, character_camera)
35        // .add_systems(Startup, setup_3d)
36        // .add_systems(Update, rotate_prism)
37        .add_systems(Startup, |mut commands: Commands| {
38            commands.spawn((Camera2d, IsDefaultUiCamera));
39        })
40        .add_systems(
41            OnEnter(AssetState::Loaded),
42            (set_icon_texture_atlas, |world: &mut World| {
43                ui_root()
44                    .update_raw_el(|raw_el| {
45                        raw_el.on_spawn_with_system(
46                            move |In(entity): In<_>,
47                                  camera: Single<Entity, With<IsDefaultUiCamera>>,
48                                  mut commands: Commands| {
49                                // https://github.com/bevyengine/bevy/discussions/11223
50                                if let Ok(mut commands) = commands.get_entity(entity) {
51                                    commands.try_insert(UiTargetCamera(*camera));
52                                }
53                            },
54                        )
55                    })
56                    .spawn(world);
57            })
58                .chain(),
59        )
60        .run();
61}
62
63const CELL_WIDTH: f32 = 70.;
64const INVENTORY_BACKGROUND_COLOR: Color = Color::hsl(0., 0., 0.78);
65const CELL_BACKGROUND_COLOR: Color = Color::hsl(0., 0., 0.55);
66const CELL_HIGHLIGHT_COLOR: Color = Color::hsl(0., 0., 0.83);
67const CELL_GAP: f32 = 5.;
68const INVENTORY_SIZE: f32 = 700.;
69const CELL_BORDER_WIDTH: f32 = 2.;
70const CELL_DARK_BORDER_COLOR: Color = Color::hsl(0., 0., 0.19);
71// const CELL_LIGHT_BORDER_COLOR: Color = Color::hsl(0., 0., 0.98);
72
73static ITEM_NAMES: LazyLock<HashMap<usize, &'static str>> = LazyLock::new(|| {
74    HashMap::from([
75        (0, "copper dagger"),
76        (1, "copper sword"),
77        (2, "shortbow"),
78        (3, "copper spear"),
79        (4, "copper axe"),
80        (5, "copper mace"),
81        (6, "copper shovel"),
82        (7, "copper pickaxe"),
83        (8, "copper hammer"),
84        (9, "copper scythe"),
85        (10, "steel dagger"),
86        (11, "steel sword"),
87        (12, "longbow"),
88        (13, "steel spear"),
89        (14, "steel axe"),
90        (15, "steel mace"),
91        (16, "steel shovel"),
92        (17, "steel pickaxe"),
93        (18, "steel hammer"),
94        (19, "steel scythe"),
95        (20, "golden dagger"),
96        (21, "golden sword"),
97        (22, "golden longbow"),
98        (23, "golden spear"),
99        (24, "golden axe"),
100        (25, "golden mace"),
101        (26, "golden shovel"),
102        (27, "golden pickaxe"),
103        (28, "golden hammer"),
104        (29, "golden scythe"),
105        (30, "copper arrow"),
106        (31, "steel arrow"),
107        (32, "golden arrow"),
108        (33, "poison arrow"),
109        (34, "fire arrow"),
110        (35, "ice arrow"),
111        (36, "electric arrow"),
112        (37, "charm arrow"),
113        (38, "leather quiver"),
114        (39, "elven quiver"),
115        (40, "apprentice robes"),
116        (41, "common shirt"),
117        (42, "copper armor"),
118        (43, "turtle buckler"),
119        (44, "wooden shield"),
120        (45, "plank shield"),
121        (46, "shoes"),
122        (47, "apprentice hat"),
123        (48, "cloth cap"),
124        (49, "copper helmet"),
125        (50, "mage robes"),
126        (51, "leather armor"),
127        (52, "steel armor"),
128        (53, "wooden buckler"),
129        (54, "reinforced wooden shield"),
130        (55, "steel shield"),
131        (56, "leather boots"),
132        (57, "mage hat"),
133        (58, "leather helmet"),
134        (59, "steel helmet"),
135        (60, "archmage robes"),
136        (61, "elven armor"),
137        (62, "golden armor"),
138        (63, "steel buckler"),
139        (64, "steel round shield"),
140        (65, "golden shield"),
141        (66, "elven boots"),
142        (67, "archmage hat"),
143        (68, "elven helmet"),
144        (69, "golden helmet"),
145        (70, "wooden staff"),
146        (71, "fire staff"),
147        (72, "lightning staff"),
148        (73, "ice staff"),
149        (74, "fire ring"),
150        (75, "lightning ring"),
151        (76, "ice ring"),
152        (77, "fire necklace"),
153        (78, "lightning necklace"),
154        (79, "ice necklace"),
155        (80, "minor healing potion"),
156        (81, "healing potion"),
157        (82, "greater healing potion"),
158        (83, "minor mana potion"),
159        (84, "mana potion"),
160        (85, "greater mana potion"),
161        (86, "yellow potion"),
162        (87, "green potion"),
163        (88, "purple potion"),
164        (89, "flying potion"),
165        (90, "gold coins (small)"),
166        (91, "gold coins (medium)"),
167        (92, "gold coins (big)"),
168        (93, "gold pouch"),
169        (94, "gold chest"),
170        (95, "ruby"),
171        (96, "topaz"),
172        (97, "emerald"),
173        (98, "sapphire"),
174        (99, "diamond"),
175        (100, "map"),
176        (101, "journal"),
177        (102, "satchel"),
178        (103, "backpack"),
179        (104, "pouch"),
180        (105, "chest (small)"),
181        (106, "chest (big)"),
182        (107, "bronze key"),
183        (108, "silver key"),
184        (109, "golden key"),
185        (110, "wood log"),
186        (111, "stone"),
187        (112, "meat"),
188        (113, "cheese"),
189        (114, "apple"),
190        (115, "poisoned apple"),
191        (116, "milk glass"),
192        (117, "egg (white)"),
193        (118, "egg (brown)"),
194        (119, "egg (golden)"),
195        (120, "carrot"),
196        (121, "berries"),
197        (122, "sunflower"),
198        (123, "flower (yellow)"),
199        (124, "flower (blue)"),
200        (125, "flower (red)"),
201        (126, "fishing rod"),
202        (127, "worm"),
203        (128, "fish_1"),
204        (129, "fish_2"),
205    ])
206});
207
208// TODO: port to Lazy
209static ICON_TEXTURE_ATLAS: OnceLock<RpgIconSheet> = OnceLock::new();
210
211// using a global handle for this so we don't need to thread the texture atlas handle through the
212// ui tree when we can guarantee it exists before any cells are inserted
213fn icon_sheet() -> &'static RpgIconSheet {
214    ICON_TEXTURE_ATLAS
215        .get()
216        .expect("expected ICON_TEXTURE_ATLAS to be initialized")
217}
218
219#[derive(AssetCollection, Resource, Clone, Debug)]
220struct RpgIconSheet {
221    #[asset(texture_atlas(tile_size_x = 48, tile_size_y = 48, columns = 10, rows = 27))]
222    layout: Handle<TextureAtlasLayout>,
223    #[asset(path = "rpg_icon_sheet.png")]
224    image: Handle<Image>,
225}
226
227fn icon(
228    index_signal: impl Signal<Item = usize> + Send + 'static,
229    count_signal: impl Signal<Item = usize> + Send + 'static,
230) -> Stack<Node> {
231    Stack::new()
232        .layer(
233            El::<ImageNode>::new()
234                .image_node(ImageNode {
235                    image: icon_sheet().image.clone(),
236                    texture_atlas: Some(TextureAtlas::from(icon_sheet().layout.clone())),
237                    ..default()
238                })
239                .on_signal_with_image_node(index_signal, |mut image_node: Mut<ImageNode>, index| {
240                    if let Some(ref mut texture_atlas) = image_node.texture_atlas {
241                        texture_atlas.index = index;
242                    }
243                }),
244        )
245        .layer(
246            El::<Text>::new()
247                .with_node(|mut node| node.top = Val::Px(6.))
248                .align(Align::new().bottom().right())
249                .text_font(TextFont::from_font_size(33.33))
250                .text_signal(count_signal.map(|count| Text(count.to_string()))),
251        )
252}
253
254#[derive(Clone, Component)]
255struct CellData {
256    index: Mutable<usize>,
257    count: Mutable<usize>,
258}
259
260#[derive(Component)]
261struct BlockClick;
262
263fn cell(cell_data_option: Mutable<Option<CellData>>, insertable: bool) -> impl Element {
264    let hovered = Mutable::new(false);
265    let original_position: Mutable<Option<Vec2>> = Mutable::new(None);
266    let down = Mutable::new(false);
267    El::<Node>::new()
268        .update_raw_el(clone!((cell_data_option, down) move |mut raw_el| {
269            if insertable {
270                raw_el = raw_el
271                .insert(Pickable::default())
272                .on_event_disableable::<Pointer<Click>, BlockClick>(
273                    clone!((cell_data_option => self_cell_data_option) move |click| {
274                        let mut consume = false;
275                        if let Some(dragging_cell_data_option) = &*DRAGGING_OPTION.lock_ref() {
276                            if self_cell_data_option.lock_ref().is_none() && let Some(dragging_cell_data) = &*dragging_cell_data_option.lock_ref() {
277                                self_cell_data_option.set(Some(CellData {
278                                    index: Mutable::new(dragging_cell_data.index.get()),
279                                    count: Mutable::new(0),
280                                }));
281                            }
282                            if let Some((dragging_cell_data, self_cell_data)) = dragging_cell_data_option.lock_ref().as_ref().zip(self_cell_data_option.lock_ref().as_ref()) {
283                                if self_cell_data.index.get() == dragging_cell_data.index.get() {
284                                    let to_add = {
285                                        if matches!(click.button, PointerButton::Secondary) {
286                                            *dragging_cell_data.count.lock_mut() -= 1;
287                                            if dragging_cell_data.count.get() == 0 {
288                                                consume = true;
289                                            }
290                                            1
291                                        } else {
292                                            let count = dragging_cell_data.count.take();
293                                            consume = true;
294                                            count
295                                        }
296                                    };
297                                    self_cell_data.count.update(|count| count + to_add);
298                                } else {
299                                    self_cell_data.index.swap(&dragging_cell_data.index);
300                                    self_cell_data.count.swap(&dragging_cell_data.count);
301                                }
302                            }
303                        }
304                        if consume && let Some(cell_data_option) = DRAGGING_OPTION.take() {
305                            cell_data_option.take();
306                        }
307                    }),
308                );
309            }
310            raw_el
311            // we don't want the click listener to trigger if we've just grabbed some of
312            // the stack as it would immediately drop one down, so we track the `Down` state
313            .on_event_with_system::<Pointer<Pressed>, _>(|In((entity, _)), mut commands: Commands| { commands.entity(entity).insert(BlockClick); })
314            .on_event_with_system::<Pointer<Released>, _>(|In((entity, _)), mut commands: Commands| { commands.entity(entity).remove::<BlockClick>(); })
315            .on_event_disableable_signal::<Pointer<Pressed>>(
316                clone!((cell_data_option, down) move |pointer_down| {
317                    let to_drag_option = {
318                        if pointer_down.button == PointerButton::Secondary {
319                            if let Some(cell_data) = &*cell_data_option.lock_ref() {
320                                let to_take = (cell_data.count.get() / 2).max(1);
321                                cell_data.count.update(|count| count - to_take);
322                                Some(CellData {
323                                    index: Mutable::new(cell_data.index.get()),
324                                    count: Mutable::new(to_take),
325                                })
326                            } else {
327                                None
328                            }
329                        } else {
330                            cell_data_option.take()
331                        }
332                    };
333                    if cell_data_option.lock_ref().as_ref().map(|cell_data| cell_data.count.get() == 0).unwrap_or(false) {
334                        cell_data_option.take();
335                    }
336                    DRAGGING_OPTION.set(Some(Mutable::new(to_drag_option)));
337                    POINTER_POSITION.set(pointer_down.pointer_location.position.into());
338                    down.set_neq(true);
339                }),
340                signal::or(is_dragging(), cell_data_option.signal_ref(Option::is_none)).dedupe()
341            )
342        }))
343        // alternative to disabling this element's cursor like what's commented out below, which may seem more intuitive, but is harder to manage due to the eventual consistency of signals
344        .cursor_signal(
345            map_ref! {
346                let &populated = cell_data_option.signal_ref(Option::is_some),
347                let &is_dragging = is_dragging() => {
348                    if is_dragging {
349                        CursorIcon::System(SystemCursorIcon::Grabbing)
350                    } else if populated {
351                        CursorIcon::System(SystemCursorIcon::Grab)
352                    } else {
353                        CursorIcon::default()
354                    }
355                }
356            }
357        )
358        // TODO: this is more idiomatic and should work, but it doesn't due to various eventual consistency shenanigans, not going to address anytime soon, use the above alternative, or manually manage components/resources to achieve the required strong consistency
359        // .cursor_disableable_signal(CursorIcon::System(SystemCursorIcon::Grab), signal::or(cell_data_option.signal_ref(Option::is_none), is_dragging()))
360        .hovered_sync(hovered.clone())
361        .with_node(|mut node| {
362            node.width = Val::Px(CELL_WIDTH);
363            node.height = Val::Px(CELL_WIDTH);
364            node.border = UiRect::all(Val::Px(CELL_BORDER_WIDTH));
365        })
366        .background_color_signal(
367            hovered.signal()
368                .map_bool(|| CELL_HIGHLIGHT_COLOR, || CELL_BACKGROUND_COLOR).map(BackgroundColor),
369        )
370        .border_color(BorderColor(CELL_DARK_BORDER_COLOR))
371        .child_signal(
372            cell_data_option
373                .signal_cloned()
374                .map_some(move |cell_data| {
375                    Stack::<Node>::new()
376                    .layer(icon(cell_data.index.signal(), cell_data.count.signal()))
377                    .layer_signal(
378                        signal::and(hovered.signal(), signal::not(is_dragging())).dedupe()
379                        .map_true(clone!((original_position) move || {
380                            El::<Node>::new()
381                                // TODO: global transform isn't populated on spawn
382                                // .with_global_transform(clone!((original_position) move |transform| original_position.set(Some(transform.compute_transform().translation.xy()))))
383                                .with_node(|mut node| {
384                                    node.height = Val::Px(CELL_WIDTH);
385                                    node.position_type = PositionType::Absolute;
386                                    node.border = UiRect::all(Val::Px(CELL_BORDER_WIDTH));
387                                    node.padding = UiRect::horizontal(Val::Px(10.));
388                                })
389                                .visibility(Visibility::Hidden)
390                                .update_raw_el(clone!((original_position) move |raw_el| {
391                                    raw_el
392                                    .on_signal_with_entity(POINTER_POSITION.signal(), move |mut entity, (mut left, mut top)| {
393                                        if let Some(transform) = entity.get::<GlobalTransform>() {
394                                            // TODO: global transform isn't populated on spawn so we have to set it here
395                                            if original_position.get().is_none() {
396                                                original_position.set(Some(transform.compute_transform().translation.xy()));
397                                            }
398                                            let original_position = original_position.get().unwrap();
399                                            left -= original_position.x - CELL_WIDTH / 2.;
400                                            top -= original_position.y + CELL_WIDTH / 2.;
401                                            // this fixes grey flash when inserting into an empty cell, which is caused by the item tooltip flashing on top before the frame it is moved
402                                            entity.insert(Visibility::Visible);
403                                        }
404                                        if let Some(mut node) = entity.get_mut::<Node>() {
405                                            node.left = Val::Px(left);
406                                            node.top = Val::Px(top);
407                                        }
408                                    })
409                                }))
410                                .global_z_index(GlobalZIndex(1))
411                                .background_color(BackgroundColor(CELL_BACKGROUND_COLOR))
412                                .border_color(BorderColor(CELL_DARK_BORDER_COLOR))
413                                .child(
414                                    El::<Text>::new()
415                                    .align(Align::center())
416                                    .text_font(TextFont::from_font_size(41.67))
417                                    .text_layout(TextLayout::new_with_no_wrap())
418                                    .text_signal(
419                                        cell_data.index.signal()
420                                        .map(|i| Text(ITEM_NAMES.get(&i).unwrap().to_string()))
421                                    )
422                                )
423                        }))
424                    )
425                })
426        )
427}
428
429fn random_cell_data(rng: &mut impl Rng) -> CellData {
430    CellData {
431        index: Mutable::new(rng.random_range(0..ITEM_NAMES.len())),
432        count: Mutable::new(rng.random_range(1..=64)),
433    }
434}
435
436fn bern_cell_data_option(bern: f64) -> Mutable<Option<CellData>> {
437    Mutable::new('block: {
438        let distribution = Bernoulli::new(bern).unwrap();
439        let mut rng = rand::rng();
440        if distribution.sample(&mut rng) {
441            break 'block Some(random_cell_data(&mut rng));
442        }
443        None
444    })
445}
446
447fn bern_cell(bern: f64, insertable: bool) -> impl Element {
448    cell(bern_cell_data_option(bern), insertable)
449}
450
451fn grid<I: IntoIterator<Item = Mutable<Option<CellData>>>>(cell_data_options: I) -> impl Element
452where
453    <I as IntoIterator>::IntoIter: std::marker::Send + 'static,
454{
455    Grid::<Node>::new()
456        .with_node(|mut node| {
457            node.width = Val::Percent(100.);
458            node.height = Val::Percent(100.);
459            node.column_gap = Val::Px(CELL_GAP);
460            node.row_gap = Val::Px(CELL_GAP);
461        })
462        .row_wrap_cell_width(CELL_WIDTH)
463        .cells(
464            cell_data_options
465                .into_iter()
466                .map(move |cell_data_option| cell(cell_data_option, true)),
467        )
468}
469
470fn set_icon_texture_atlas(rpg_icon_sheet: Res<RpgIconSheet>) {
471    ICON_TEXTURE_ATLAS
472        .set(rpg_icon_sheet.clone())
473        .expect("failed to initialize ICON_TEXTURE_ATLAS");
474}
475
476// fn character_camera(mut commands: Commands) {
477//     // https://github.com/bevyengine/bevy/discussions/11223
478//     commands.spawn((
479//         Camera3d::default(),
480//         Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),
481//         Camera {
482//             order: 1,
483//             clear_color: ClearColorConfig::None,
484//             ..default()
485//         },
486//         RenderLayers::layer(1),
487//     ));
488// }
489
490// fn setup_3d(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials:
491// ResMut<Assets<StandardMaterial>>) {     // Add a light source
492//     commands.spawn(PointLight {
493//         intensity: 1500.0,
494//         shadows_enabled: true,
495//         ..default()
496//     })
497//     .insert(Transform::from_xyz(4.0, 8.0, 4.0));
498
499//     // Spawn the rotating rectangular prism
500//     commands.spawn((
501//         Mesh3d(meshes.add(Cuboid::default())),
502//         MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
503//         Transform::from_scale(Vec3::new(1.0, 1.5, 0.5)),
504//         RotatingPrism,
505//         RenderLayers::layer(1),
506//     ));
507// }
508
509// fn rotate_prism(time: Res<Time>, mut query: Query<&mut Transform, With<RotatingPrism>>) {
510//     for mut transform in query.iter_mut() {
511//         transform.rotation *= Quat::from_rotation_y(1.0 * time.delta_secs());
512//     }
513// }
514
515// #[derive(Component)]
516// struct RotatingPrism;
517
518fn dot() -> impl Element {
519    El::<Node>::new()
520        .with_node(|mut node| {
521            node.width = Val::Px(CELL_BORDER_WIDTH * 2.);
522            node.height = Val::Px(CELL_BORDER_WIDTH * 2.);
523        })
524        .background_color(BackgroundColor(CELL_BACKGROUND_COLOR))
525}
526
527fn dot_row(n: usize) -> impl Element {
528    Row::<Node>::new().items((0..n).map(|_| dot()))
529}
530
531fn arrow() -> impl Element {
532    Column::<Node>::new()
533        .align_content(Align::center())
534        .items((0..=6).map(|i| dot_row(2 * i + 1)))
535        .items((0..6).map(|_| dot_row(3)))
536}
537
538fn side_column() -> impl Element {
539    Column::<Node>::new()
540        .with_node(|mut node| node.row_gap = Val::Px(CELL_GAP))
541        .items((0..4).map(|_| bern_cell(0.5, true)))
542}
543
544fn inventory() -> impl Element {
545    El::<Node>::new()
546        .align(Align::center())
547        .with_node(|mut node| {
548            node.height = Val::Px(INVENTORY_SIZE);
549            node.width = Val::Px(INVENTORY_SIZE);
550        })
551        .child(
552            Column::<Node>::new()
553                .with_node(|mut node| {
554                    node.height = Val::Percent(100.);
555                    node.width = Val::Percent(100.);
556                    node.row_gap = Val::Px(CELL_GAP * 4.);
557                })
558                .background_color(BackgroundColor(INVENTORY_BACKGROUND_COLOR))
559                .align_content(Align::center())
560                .item(
561                    Row::<Node>::new()
562                        .with_node(|mut node| {
563                            node.width = Val::Percent(100.);
564                            node.column_gap = Val::Px(CELL_GAP);
565                        })
566                        .item(
567                            Row::<Node>::new()
568                                .align_content(Align::center())
569                                .with_node(|mut node| {
570                                    node.width = Val::Percent(60.);
571                                    node.column_gap = Val::Px(CELL_GAP);
572                                    node.padding = UiRect::horizontal(Val::Px(CELL_GAP * 3.));
573                                })
574                                .item(side_column())
575                                .item(
576                                    El::<Node>::new()
577                                        .with_node(|mut node| {
578                                            node.height = Val::Px(CELL_WIDTH * 4. + CELL_GAP * 3.);
579                                            node.width = Val::Percent(100.);
580                                        })
581                                        .background_color(BackgroundColor(Color::BLACK)),
582                                )
583                                .item(side_column())
584                        )
585                        .item(
586                            El::<Node>::new()
587                                .with_node(|mut node| {
588                                    node.width = Val::Percent(40.);
589                                    node.height = Val::Percent(100.);
590                                })
591                                .align_content(Align::center())
592                                .child({
593                                    let inputs = MutableVec::new_with_values(
594                                        (0..4).map(|_| bern_cell_data_option(0.2)).collect(),
595                                    );
596                                    let output: Mutable<Option<CellData>> = default();
597                                    let outputter = spawn(clone!((inputs, output) async move {
598                                        // TODO: explain every step of this signal
599                                        inputs.signal_vec_cloned()
600                                        .map_signal(|input|
601                                            input.signal_cloned()
602                                            // this says "retrigger" the outputter every time any of the input's
603                                            // texture atlas index or count changes
604                                            .map_some(|cell_data| map_ref! {
605                                                let _ = cell_data.index.signal_ref(|_|()),
606                                                let _ = cell_data.count.signal_ref(|_|()) => ()
607                                            })
608                                            .switch(signal::option)
609                                        )
610                                        .to_signal_map(|filleds| filleds.iter().all(Option::is_some))
611                                        .for_each_sync(move |all_filled| {
612                                            output.set(all_filled.then(|| random_cell_data(&mut rand::rng())));
613                                        })
614                                        .await;
615                                    }));
616                                    Column::<Node>::new()
617                                        .update_raw_el(|raw_el| raw_el.hold_tasks([outputter]))
618                                        .with_node(|mut node| {
619                                            node.row_gap = Val::Px(CELL_GAP * 2.);
620                                        })
621                                        .item(
622                                            // need to add another wrapping node here so the special output `Down`
623                                            // handler doesn't overwrite the default `cell` `Down` handler
624                                            El::<Node>::new()
625                                            .child(cell(output.clone(), false).align(Align::center()))
626                                            .update_raw_el(clone!((inputs) move |raw_el| {
627                                                raw_el
628                                                .on_event_disableable_signal::<Pointer<Pressed>>(
629                                                    clone!((inputs) move |_| {
630                                                        for input in inputs.lock_ref().iter() {
631                                                            input.take();
632                                                        }
633                                                    }),
634                                                    signal::not(signal::and(DRAGGING_OPTION.signal_ref(Option::is_none), output.signal_ref(Option::is_some))).dedupe()
635                                                )
636                                            }))
637                                        )
638                                        .item(arrow())
639                                        .item({
640                                            let cell_data_options = inputs.lock_ref().iter().cloned().collect::<Vec<_>>();
641                                            El::<Node>::new()
642                                                .with_node(|mut node| node.width = Val::Px(CELL_WIDTH * 2. + CELL_GAP))
643                                                .child(grid(cell_data_options).align_content(Align::new().center_x()))
644                                        })
645                                }),
646                        ),
647                )
648                .item(
649                    El::<Node>::new()
650                        .with_node(|mut node| node.width = Val::Percent(100.))
651                        .child(
652                            grid((0..27).map(|_| bern_cell_data_option(0.5)))
653                                .align_content(Align::new().center_x()),
654                        ),
655                )
656                .item(
657                    Row::<Node>::new()
658                        .with_node(|mut node| {
659                            node.column_gap = Val::Px(CELL_GAP);
660                        })
661                        .items((0..9).map(|_| bern_cell(0.5, true))),
662                ),
663        )
664}
665
666static DRAGGING_OPTION: LazyLock<Mutable<Option<Mutable<Option<CellData>>>>> = LazyLock::new(default);
667
668static POINTER_POSITION: LazyLock<Mutable<(f32, f32)>> = LazyLock::new(default);
669
670fn is_dragging() -> impl Signal<Item = bool> {
671    DRAGGING_OPTION.signal_ref(Option::is_some)
672}
673
674fn ui_root() -> impl Element {
675    Stack::<Node>::new()
676        .cursor_disableable_signal(CursorIcon::default(), is_dragging())
677        .with_node(|mut node| {
678            node.width = Val::Percent(100.);
679            node.height = Val::Percent(100.);
680        })
681        .update_raw_el(|raw_el| {
682            raw_el
683                .on_event_with_system::<Pointer<Move>, _>(|In((_, move_)): In<(_, Pointer<Move>)>| {
684                    POINTER_POSITION.set(move_.pointer_location.position.into());
685                })
686                .component_signal::<Pickable, _>(is_dragging().map_true(default))
687        })
688        .align_content(Align::center())
689        .layer(inventory())
690        .layer_signal(
691            DRAGGING_OPTION
692                .signal_cloned()
693                .map_some(|cell_data_option| cell_data_option.signal_cloned())
694                .switch(signal::option)
695                .map(Option::flatten)
696                .map_some(move |cell_data| {
697                    icon(cell_data.index.signal(), cell_data.count.signal())
698                        .update_raw_el(|raw_el| {
699                            raw_el.defer_update(DeferredUpdaterAppendDirection::Front, |raw_el| {
700                                raw_el.insert(Pickable {
701                                    // required to allow cell hover to leak through a dragging icon
702                                    should_block_lower: false,
703                                    is_hoverable: true,
704                                })
705                            })
706                        })
707                        .cursor(CursorIcon::System(SystemCursorIcon::Grabbing))
708                        .with_node(|mut node| {
709                            node.width = Val::Px(CELL_WIDTH);
710                            node.height = Val::Px(CELL_WIDTH);
711                            node.position_type = PositionType::Absolute;
712                            let pointer_position = POINTER_POSITION.get();
713                            // TODO: this is actually *extremely* cringe, because the `.on_signal_with_node`
714                            // will(might?) not tick before the first frame the icon is
715                            // rendered, the icon will flash from the left middle of the screen (default absolute
716                            // position?) to the pointer position, this means that the
717                            // position must first be set statically here *and* in reaction
718                            // to the pointer position below; workaround could be to wait
719                            // for a tick before making the the element visible, but
720                            // *ideally* we would force all signals to tick before the first frame, but not
721                            // sure if that's possible
722                            set_dragging_position(node, pointer_position);
723                        })
724                        .global_z_index(GlobalZIndex(1))
725                        .on_signal_with_node(POINTER_POSITION.signal(), set_dragging_position)
726                }),
727        )
728}
729
730fn set_dragging_position(mut node: Mut<Node>, pointer_position: (f32, f32)) {
731    node.left = Val::Px(pointer_position.0 - CELL_WIDTH / 2.);
732    node.top = Val::Px(pointer_position.1 - CELL_WIDTH / 2.);
733}
734
735#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
736enum AssetState {
737    #[default]
738    Loading,
739    Loaded,
740}