Skip to main content

pointer_events/
pointer-events.rs

1/// Pointer events demo — Click, DragMove, DragEnd.
2///
3/// Three groups of interactive cubes, each wired to a different gesture subset:
4///
5///  LEFT  — "click" cubes: flash color on Click; ignore drags.
6///  MID   — "drag" cube: follows the pointer while dragging.
7///  RIGHT — "both" cube: drag to reposition AND click to cycle color.
8///
9/// This example is the primary test bed for EventSignal::Click, which is
10/// emitted by GestureSystem at DragEnd time when pointer travel < 8 px.
11use mittens_engine::{engine, utils};
12
13#[path = "example_util/mod.rs"]
14mod example_util;
15
16use std::sync::{Mutex, OnceLock};
17
18// ---------------------------------------------------------------------------
19// Shared click-cube state
20// ---------------------------------------------------------------------------
21
22#[derive(Debug)]
23struct ClickCubeState {
24    color_node: engine::ecs::ComponentId,
25    click_count: u32,
26}
27
28static CLICK_CUBES: OnceLock<
29    Mutex<std::collections::HashMap<engine::ecs::ComponentId, ClickCubeState>>,
30> = OnceLock::new();
31
32fn click_cube_colors() -> &'static [[f32; 4]] {
33    &[
34        [0.25, 0.55, 1.00, 1.0], // blue (default)
35        [1.00, 0.35, 0.35, 1.0], // red
36        [0.30, 0.85, 0.45, 1.0], // green
37        [1.00, 0.80, 0.15, 1.0], // yellow
38        [0.75, 0.30, 1.00, 1.0], // purple
39    ]
40}
41
42fn on_click_cube(
43    world: &mut engine::ecs::World,
44    emit: &mut dyn engine::ecs::SignalEmitter,
45    env: &engine::ecs::Signal,
46) {
47    if !matches!(
48        env.event.as_ref(),
49        Some(engine::ecs::EventSignal::Click { .. })
50    ) {
51        return;
52    }
53
54    // env.scope is the hit renderable; walk up to find the cube root in the map.
55    let map = CLICK_CUBES.get_or_init(|| Mutex::new(Default::default()));
56    let mut guard = map.lock().unwrap();
57    let mut cur = Some(env.scope);
58    let mut found_root = None;
59    while let Some(id) = cur {
60        if guard.contains_key(&id) {
61            found_root = Some(id);
62            break;
63        }
64        cur = world.parent_of(id);
65    }
66    let Some(root) = found_root else { return };
67    let Some(state) = guard.get_mut(&root) else {
68        return;
69    };
70
71    state.click_count += 1;
72    let colors = click_cube_colors();
73    let rgba = colors[state.click_count as usize % colors.len()];
74
75    emit.push_intent_now(
76        root,
77        engine::ecs::IntentValue::SetColor {
78            component_ids: vec![state.color_node],
79            rgba,
80        },
81    );
82}
83
84fn spawn_click_cube(
85    universe: &mut engine::Universe,
86    parent: engine::ecs::ComponentId,
87    pos: [f32; 3],
88) -> engine::ecs::ComponentId {
89    use engine::ecs::component::{
90        ColorComponent, RaycastableComponent, RenderableComponent, TransformComponent,
91    };
92
93    let root = universe.world.add_component(
94        TransformComponent::new()
95            .with_position(pos[0], pos[1], pos[2])
96            .with_scale(0.40, 0.40, 0.40),
97    );
98    let color_node = universe
99        .world
100        .add_component(ColorComponent::rgba(0.25, 0.55, 1.0, 1.0));
101    let r = universe.world.add_component(RenderableComponent::cube());
102    let rc = universe
103        .world
104        .add_component(RaycastableComponent::enabled());
105
106    // Build the subtree before connecting to the live parent so that
107    // RaycastableComponent is already a child of RenderableComponent when
108    // RegisterRenderable is processed (renderable_is_raycastable checks children).
109    let _ = universe.attach(root, color_node);
110    let _ = universe.attach(color_node, r);
111    let _ = universe.attach(r, rc);
112    let _ = universe.attach(parent, root);
113
114    CLICK_CUBES
115        .get_or_init(|| Mutex::new(Default::default()))
116        .lock()
117        .unwrap()
118        .insert(
119            root,
120            ClickCubeState {
121                color_node,
122                click_count: 0,
123            },
124        );
125
126    universe.add_signal_handler(engine::ecs::SignalKind::Click, root, on_click_cube);
127
128    root
129}
130
131// ---------------------------------------------------------------------------
132// Drag cube — follows pointer while held
133// ---------------------------------------------------------------------------
134
135fn on_drag_move(
136    world: &mut engine::ecs::World,
137    emit: &mut dyn engine::ecs::SignalEmitter,
138    env: &engine::ecs::Signal,
139) {
140    let Some(engine::ecs::EventSignal::DragMove { delta_world, .. }) = env.event.as_ref() else {
141        return;
142    };
143    let delta = *delta_world;
144
145    // Walk up from the hit renderable to find the TransformComponent to move.
146    let mut cur = Some(env.scope);
147    while let Some(id) = cur {
148        if world
149            .get_component_by_id_as::<engine::ecs::component::TransformComponent>(id)
150            .is_some()
151        {
152            let [x, y, z] = world
153                .get_component_by_id_as::<engine::ecs::component::TransformComponent>(id)
154                .map(|t| t.transform.translation)
155                .unwrap_or([0.0; 3]);
156
157            emit.push_intent_now(
158                id,
159                engine::ecs::IntentValue::SetPosition {
160                    component_ids: vec![id],
161                    position: [x + delta[0], y + delta[1], z + delta[2]],
162                },
163            );
164            return;
165        }
166        cur = world.parent_of(id);
167    }
168}
169
170fn spawn_drag_cube(
171    universe: &mut engine::Universe,
172    parent: engine::ecs::ComponentId,
173    pos: [f32; 3],
174    color: [f32; 4],
175) -> engine::ecs::ComponentId {
176    use engine::ecs::component::{
177        ColorComponent, RaycastableComponent, RenderableComponent, TransformComponent,
178    };
179
180    let root = universe.world.add_component(
181        TransformComponent::new()
182            .with_position(pos[0], pos[1], pos[2])
183            .with_scale(0.45, 0.45, 0.45),
184    );
185    let color_node = universe
186        .world
187        .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
188    let r = universe.world.add_component(RenderableComponent::cube());
189    let rc = universe
190        .world
191        .add_component(RaycastableComponent::enabled());
192
193    let _ = universe.attach(root, color_node);
194    let _ = universe.attach(color_node, r);
195    let _ = universe.attach(r, rc);
196    let _ = universe.attach(parent, root);
197
198    universe.add_signal_handler(engine::ecs::SignalKind::DragMove, r, on_drag_move);
199
200    root
201}
202
203// ---------------------------------------------------------------------------
204// "Both" cube — drag to move, click to cycle color
205// ---------------------------------------------------------------------------
206
207#[derive(Debug)]
208struct BothCubeState {
209    color_node: engine::ecs::ComponentId,
210    click_count: u32,
211}
212
213static BOTH_CUBES: OnceLock<
214    Mutex<std::collections::HashMap<engine::ecs::ComponentId, BothCubeState>>,
215> = OnceLock::new();
216
217fn on_both_cube(
218    world: &mut engine::ecs::World,
219    emit: &mut dyn engine::ecs::SignalEmitter,
220    env: &engine::ecs::Signal,
221) {
222    match env.event.as_ref() {
223        Some(engine::ecs::EventSignal::DragMove { delta_world, .. }) => {
224            on_drag_move(world, emit, env);
225            let _ = delta_world;
226        }
227        Some(engine::ecs::EventSignal::Click { .. }) => {
228            let map = BOTH_CUBES.get_or_init(|| Mutex::new(Default::default()));
229            let mut guard = map.lock().unwrap();
230            // scope is the renderable; walk up to find the cube root registered in the map
231            let mut cur = Some(env.scope);
232            let mut found_root = None;
233            while let Some(id) = cur {
234                if guard.contains_key(&id) {
235                    found_root = Some(id);
236                    break;
237                }
238                cur = world.parent_of(id);
239            }
240            let Some(root) = found_root else { return };
241            let Some(state) = guard.get_mut(&root) else {
242                return;
243            };
244
245            state.click_count += 1;
246            let colors = click_cube_colors();
247            let rgba = colors[state.click_count as usize % colors.len()];
248            emit.push_intent_now(
249                root,
250                engine::ecs::IntentValue::SetColor {
251                    component_ids: vec![state.color_node],
252                    rgba,
253                },
254            );
255        }
256        _ => {}
257    }
258}
259
260fn spawn_both_cube(
261    universe: &mut engine::Universe,
262    parent: engine::ecs::ComponentId,
263    pos: [f32; 3],
264) -> engine::ecs::ComponentId {
265    use engine::ecs::component::{
266        ColorComponent, RaycastableComponent, RenderableComponent, TransformComponent,
267    };
268
269    let root = universe.world.add_component(
270        TransformComponent::new()
271            .with_position(pos[0], pos[1], pos[2])
272            .with_scale(0.45, 0.45, 0.45),
273    );
274    let color_node = universe
275        .world
276        .add_component(ColorComponent::rgba(0.95, 0.60, 0.20, 1.0));
277    let r = universe.world.add_component(RenderableComponent::cube());
278    let rc = universe
279        .world
280        .add_component(RaycastableComponent::enabled());
281
282    let _ = universe.attach(root, color_node);
283    let _ = universe.attach(color_node, r);
284    let _ = universe.attach(r, rc);
285    let _ = universe.attach(parent, root);
286
287    BOTH_CUBES
288        .get_or_init(|| Mutex::new(Default::default()))
289        .lock()
290        .unwrap()
291        .insert(
292            root,
293            BothCubeState {
294                color_node,
295                click_count: 0,
296            },
297        );
298
299    universe.add_signal_handler(engine::ecs::SignalKind::DragMove, r, on_both_cube);
300    universe.add_signal_handler(engine::ecs::SignalKind::Click, r, on_both_cube);
301
302    root
303}
304
305// ---------------------------------------------------------------------------
306// Label helper
307// ---------------------------------------------------------------------------
308
309fn spawn_label(
310    universe: &mut engine::Universe,
311    parent: engine::ecs::ComponentId,
312    pos: [f32; 3],
313    text: &str,
314) {
315    use engine::ecs::component::{
316        ColorComponent, EmissiveComponent, TextComponent, TextureFilteringComponent,
317        TransformComponent, TransparentCutoutComponent,
318    };
319
320    let root = universe.world.add_component(
321        TransformComponent::new()
322            .with_position(pos[0], pos[1], pos[2])
323            .with_scale(0.055, 0.055, 1.0),
324    );
325    let txt = universe.world.add_component(TextComponent::new(text));
326    let color = universe
327        .world
328        .add_component(ColorComponent::rgba(0.05, 0.05, 0.08, 1.0));
329    let emissive = universe.world.add_component(EmissiveComponent::on());
330    let filtering = universe
331        .world
332        .add_component(TextureFilteringComponent::nearest_magnification());
333    let cutout = universe
334        .world
335        .add_component(TransparentCutoutComponent::new());
336
337    let _ = universe.attach(parent, root);
338    let _ = universe.attach(root, txt);
339    let _ = universe.attach(txt, color);
340    let _ = universe.attach(txt, emissive);
341    let _ = universe.attach(txt, filtering);
342    let _ = universe.attach(txt, cutout);
343}
344
345// ---------------------------------------------------------------------------
346// main
347// ---------------------------------------------------------------------------
348
349fn main() {
350    mittens_engine::example_support::ensure_model_assets();
351    utils::logger::init();
352
353    let world = engine::ecs::World::default();
354    let mut universe = engine::Universe::new(world);
355
356    use engine::ecs::component::{
357        AmbientLightComponent, BackgroundColorComponent, Camera3DComponent, ColorComponent,
358        DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
359        TransformComponent,
360    };
361
362    // Background.
363    let bg = universe
364        .world
365        .add_component(BackgroundColorComponent::new());
366    let bg_c = universe
367        .world
368        .add_component(ColorComponent::rgba(0.12, 0.12, 0.16, 1.0));
369    let _ = universe.world.add_child(bg, bg_c);
370    universe.add(bg);
371
372    // Lighting.
373    let ambient = universe
374        .world
375        .add_component(AmbientLightComponent::rgb(0.30, 0.30, 0.32));
376    universe.add(ambient);
377
378    let sun_t = universe
379        .world
380        .add_component(TransformComponent::new().with_position(2.0, 4.0, 3.0));
381    let sun = universe
382        .world
383        .add_component(DirectionalLightComponent::new());
384    let _ = universe.attach(sun_t, sun);
385    universe.add(sun_t);
386
387    // Camera + pointer rig.
388    let input = universe
389        .world
390        .add_component(InputComponent::new().with_speed(3.0));
391    let input_mode = universe.world.add_component(
392        InputTransformModeComponent::forward_z()
393            .with_fps_rotation()
394            .with_roll_axis_y(),
395    );
396    let _ = universe.attach(input, input_mode);
397
398    let cam_t = universe
399        .world
400        .add_component(TransformComponent::new().with_position(0.0, 0.5, 4.5));
401    let cam = universe
402        .world
403        .add_component(Camera3DComponent::new().with_fov(70.0));
404    let pointer = universe.world.add_component(PointerComponent::new());
405
406    let _ = universe.attach(input, cam_t);
407    let _ = universe.attach(cam_t, cam);
408    let _ = universe.attach(cam, pointer);
409
410    example_util::spawn_desktop_camera_controls_hint(&mut universe, cam_t);
411    universe.add(input);
412
413    // Scene root for all interactive objects.
414    let scene = universe.world.add_component(TransformComponent::new());
415    universe.add(scene);
416
417    // --- LEFT: click-only cubes ---
418    let click_group = universe
419        .world
420        .add_component(TransformComponent::new().with_position(-2.2, 0.0, 0.0));
421    let _ = universe.attach(scene, click_group);
422
423    for (i, dy) in [-0.55_f32, 0.0, 0.55].iter().enumerate() {
424        spawn_click_cube(&mut universe, click_group, [0.0, *dy, 0.0]);
425        let _ = i;
426    }
427    spawn_label(
428        &mut universe,
429        click_group,
430        [-0.25, -1.05, 0.0],
431        "click\ncycles color",
432    );
433
434    // --- MID: drag-only cube ---
435    let drag_cube_root = universe
436        .world
437        .add_component(TransformComponent::new().with_position(0.0, 0.0, 0.0));
438    let _ = universe.attach(scene, drag_cube_root);
439
440    spawn_drag_cube(
441        &mut universe,
442        drag_cube_root,
443        [0.0, 0.0, 0.0],
444        [0.35, 0.85, 0.55, 1.0],
445    );
446    spawn_label(
447        &mut universe,
448        drag_cube_root,
449        [-0.25, -0.80, 0.0],
450        "drag\nto move",
451    );
452
453    // --- RIGHT: drag + click cube ---
454    let both_root = universe
455        .world
456        .add_component(TransformComponent::new().with_position(2.2, 0.0, 0.0));
457    let _ = universe.attach(scene, both_root);
458
459    spawn_both_cube(&mut universe, both_root, [0.0, 0.0, 0.0]);
460    spawn_label(
461        &mut universe,
462        both_root,
463        [-0.30, -0.80, 0.0],
464        "drag to move\nclick for color",
465    );
466
467    universe.systems.process_commands(
468        &mut universe.world,
469        &mut universe.visuals,
470        &mut universe.render_assets,
471        &mut universe.command_queue,
472    );
473
474    universe.enable_repl();
475    engine::Windowing::run_app(universe).expect("Windowing failed");
476}