1use mittens_engine::{engine, utils};
2
3#[path = "example_util/mod.rs"]
4mod example_util;
5
6use std::collections::HashMap;
7use std::sync::{Mutex, OnceLock};
8
9#[derive(Debug, Clone, Copy)]
10struct ButtonBorder {
11 left: f32,
12 top: f32,
13 right: f32,
14 bottom: f32,
15}
16
17impl ButtonBorder {
18 fn new(left: f32, top: f32, right: f32, bottom: f32) -> Self {
19 Self {
20 left,
21 top,
22 right,
23 bottom,
24 }
25 }
26
27 fn uniform(v: f32) -> Self {
28 Self {
29 left: v,
30 top: v,
31 right: v,
32 bottom: v,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
38struct ButtonState {
39 pressed: bool,
40
41 cap_root: engine::ecs::ComponentId,
42 cap_raised_z: f32,
43 cap_pressed_z: f32,
44
45 frame_color: engine::ecs::ComponentId,
46 face_color: engine::ecs::ComponentId,
47
48 text_id: engine::ecs::ComponentId,
49 text_color: engine::ecs::ComponentId,
50
51 frame_color_up: [f32; 4],
52 face_color_up: [f32; 4],
53 text_color_up: [f32; 4],
54
55 frame_color_down: [f32; 4],
56 face_color_down: [f32; 4],
57 text_color_down: [f32; 4],
58}
59
60static BUTTONS: OnceLock<Mutex<HashMap<engine::ecs::ComponentId, ButtonState>>> = OnceLock::new();
61
62fn darken([r, g, b, a]: [f32; 4], factor: f32) -> [f32; 4] {
63 [r * factor, g * factor, b * factor, a]
64}
65
66fn set_button_pressed(
67 world: &mut engine::ecs::World,
68 emit: &mut dyn engine::ecs::SignalEmitter,
69 scope: engine::ecs::ComponentId,
70 pressed: bool,
71) {
72 let buttons = BUTTONS.get_or_init(|| Mutex::new(HashMap::new()));
73 let mut guard = buttons.lock().expect("button state lock");
74
75 let mut cursor = Some(scope);
78 let mut button_root = None;
79 while let Some(node) = cursor {
80 if guard.contains_key(&node) {
81 button_root = Some(node);
82 break;
83 }
84 cursor = world.parent_of(node);
85 }
86 let Some(button_root) = button_root else {
87 return;
88 };
89
90 let Some(state) = guard.get_mut(&button_root) else {
91 return;
92 };
93
94 if state.pressed == pressed {
95 return;
96 }
97 state.pressed = pressed;
98
99 let (frame_rgba, face_rgba, text_rgba, cap_z) = if pressed {
100 (
101 state.frame_color_down,
102 state.face_color_down,
103 state.text_color_down,
104 state.cap_pressed_z,
105 )
106 } else {
107 (
108 state.frame_color_up,
109 state.face_color_up,
110 state.text_color_up,
111 state.cap_raised_z,
112 )
113 };
114
115 emit.push_intent_now(
116 button_root,
117 engine::ecs::IntentValue::SetColor {
118 component_ids: vec![state.frame_color],
119 rgba: frame_rgba,
120 },
121 );
122 emit.push_intent_now(
123 button_root,
124 engine::ecs::IntentValue::SetColor {
125 component_ids: vec![state.face_color],
126 rgba: face_rgba,
127 },
128 );
129
130 emit.push_intent_now(
133 button_root,
134 engine::ecs::IntentValue::RemoveSubtree {
135 component_ids: vec![state.text_color],
136 },
137 );
138 let new_text_color =
139 world.add_component(engine::ecs::component::ColorComponent { rgba: text_rgba });
140 emit.push_intent_now(
141 button_root,
142 engine::ecs::IntentValue::Attach {
143 parents: vec![state.text_id],
144 child: new_text_color,
145 },
146 );
147 state.text_color = new_text_color;
148
149 emit.push_intent_now(
152 button_root,
153 engine::ecs::IntentValue::SetPosition {
154 component_ids: vec![state.cap_root],
155 position: [0.0, 0.0, cap_z],
156 },
157 );
158}
159
160fn button_press_handler(
161 world: &mut engine::ecs::World,
162 emit: &mut dyn engine::ecs::SignalEmitter,
163 env: &engine::ecs::Signal,
164) {
165 match env.event.as_ref() {
166 Some(engine::ecs::EventSignal::DragStart { .. }) => {
167 set_button_pressed(world, emit, env.scope, true);
168 }
169 Some(engine::ecs::EventSignal::DragEnd { .. }) => {
170 set_button_pressed(world, emit, env.scope, false);
171 }
172 _ => {}
173 }
174}
175
176fn spawn_raycastable_cube(
177 universe: &mut engine::Universe,
178 parent: engine::ecs::ComponentId,
179 pos: [f32; 3],
180 scale: [f32; 3],
181) -> engine::ecs::ComponentId {
182 use engine::ecs::component::{RaycastableComponent, RenderableComponent, TransformComponent};
183
184 let t = universe.world.add_component(
185 TransformComponent::new()
186 .with_position(pos[0], pos[1], pos[2])
187 .with_scale(scale[0], scale[1], scale[2]),
188 );
189 let r = universe.world.add_component(RenderableComponent::cube());
190 let rc = universe
191 .world
192 .add_component(RaycastableComponent::enabled());
193
194 let _ = universe.attach(parent, t);
195 let _ = universe.attach(t, r);
196 let _ = universe.attach(r, rc);
197
198 r
199}
200
201fn spawn_raycastable_colored_cube(
202 universe: &mut engine::Universe,
203 parent: engine::ecs::ComponentId,
204 pos: [f32; 3],
205 scale: [f32; 3],
206 rgba: [f32; 4],
207) -> engine::ecs::ComponentId {
208 use engine::ecs::component::ColorComponent;
209
210 let r = spawn_raycastable_cube(universe, parent, pos, scale);
211 let c = universe
212 .world
213 .add_component(ColorComponent::rgba(rgba[0], rgba[1], rgba[2], rgba[3]));
214 let _ = universe.attach(r, c);
215 r
216}
217
218fn wrap_at_for_button_middle_width(middle_w: f32, text_scale: f32) -> usize {
219 let padding_world = (middle_w * 0.10).clamp(0.02, 0.25);
223 let usable_world = (middle_w - padding_world).max(0.05);
224 let glyph_w_world = text_scale.abs().max(1e-4);
225
226 let chars_per_line = (usable_world / glyph_w_world).floor() as isize;
227 (chars_per_line.max(1) as usize).min(200)
228}
229
230fn measure_word_wrapped_block(text: &str, wrap_at: usize) -> (usize, usize) {
231 if text.is_empty() {
234 return (0, 0);
235 }
236
237 let wrap_at = wrap_at.max(1);
238
239 let mut rows = 1usize;
240 let mut col = 0usize;
241 let mut max_col = 0usize;
242
243 for (wi, word) in text.split_whitespace().enumerate() {
244 let wlen = word.chars().count();
245 if wlen == 0 {
246 continue;
247 }
248
249 let needs_space = col > 0;
251 let add = wlen + if needs_space { 1 } else { 0 };
252
253 if col > 0 && col + add > wrap_at {
254 max_col = max_col.max(col);
256 rows += 1;
257 col = 0;
258 }
259
260 col += add;
262
263 max_col = max_col.max(col);
265
266 if wi == 0 {
268 }
270 }
271
272 max_col = max_col.max(col);
273 (max_col, rows)
274}
275
276fn spawn_button(
277 universe: &mut engine::Universe,
278 pos: [f32; 3],
279 middle_wh: [f32; 2],
280 border: ButtonBorder,
281 text: &str,
282 text_scale: f32,
283 color: [f32; 4],
284 background_color: [f32; 4],
285) -> engine::ecs::ComponentId {
286 use engine::ecs::component::{
287 ColorComponent, TextComponent, TextureFilteringComponent, TransformComponent,
288 TransparentCutoutComponent,
289 };
290
291 let button_root = universe
292 .world
293 .add_component(TransformComponent::new().with_position(pos[0], pos[1], pos[2]));
294
295 let frame_root = universe.world.add_component(TransformComponent::new());
297 let frame_color = universe
298 .world
299 .add_component(ColorComponent::rgba(color[0], color[1], color[2], color[3]));
300 let _ = universe.attach(button_root, frame_root);
301 let _ = universe.attach(frame_root, frame_color);
302
303 let cap_raised_z = 0.030;
305 let cap_pressed_z = 0.000;
306 let cap_root = universe
307 .world
308 .add_component(TransformComponent::new().with_position(0.0, 0.0, cap_raised_z));
309 let _ = universe.attach(button_root, cap_root);
310
311 let face_color = universe.world.add_component(ColorComponent::rgba(
312 background_color[0],
313 background_color[1],
314 background_color[2],
315 background_color[3],
316 ));
317 let _ = universe.attach(cap_root, face_color);
318
319 let depth = 0.025;
321 let middle_w = middle_wh[0];
322 let middle_h = middle_wh[1];
323
324 let full_w = middle_w + border.left + border.right;
326 let full_h = middle_h + border.top + border.bottom;
327
328 let _left = spawn_raycastable_cube(
330 universe,
331 frame_color,
332 [-(middle_w * 0.5 + border.left * 0.5), 0.0, 0.0],
333 [border.left, full_h, depth],
334 );
335 let _right = spawn_raycastable_cube(
336 universe,
337 frame_color,
338 [(middle_w * 0.5 + border.right * 0.5), 0.0, 0.0],
339 [border.right, full_h, depth],
340 );
341
342 let _top = spawn_raycastable_cube(
344 universe,
345 frame_color,
346 [0.0, (middle_h * 0.5 + border.top * 0.5), 0.0],
347 [full_w, border.top, depth],
348 );
349 let _bottom = spawn_raycastable_cube(
350 universe,
351 frame_color,
352 [0.0, -(middle_h * 0.5 + border.bottom * 0.5), 0.0],
353 [full_w, border.bottom, depth],
354 );
355
356 let _face = spawn_raycastable_cube(
358 universe,
359 face_color,
360 [0.0, 0.0, 0.0],
361 [middle_w, middle_h, depth],
362 );
363
364 let text_root = universe.world.add_component(
366 TransformComponent::new()
367 .with_position(0.0, 0.0, depth * 0.5 + 0.006)
368 .with_scale(text_scale, text_scale, 1.0),
369 );
370 let _ = universe.attach(cap_root, text_root);
371
372 let wrap_at = wrap_at_for_button_middle_width(middle_w, text_scale);
373 let (cols, rows) = measure_word_wrapped_block(text, wrap_at);
374 let cols_f = cols.max(1) as f32;
375 let rows_f = rows.max(1) as f32;
376
377 let x_center = -0.5 * (cols_f - 1.0);
380 let y_center = 0.5 * (rows_f - 1.0);
381 let text_offset = universe
382 .world
383 .add_component(TransformComponent::new().with_position(x_center, y_center, 0.0));
384 let _ = universe.attach(text_root, text_offset);
385
386 let text_id = universe
387 .world
388 .add_component(TextComponent::with_word_wrap(text, wrap_at));
389 let _ = universe.attach(text_offset, text_id);
390
391 let cutout = universe
392 .world
393 .add_component(TransparentCutoutComponent::new());
394 let _ = universe.attach(text_id, cutout);
395
396 let filtering = universe
397 .world
398 .add_component(TextureFilteringComponent::nearest_magnification());
399 let _ = universe.attach(text_id, filtering);
400
401 universe.add(button_root);
402
403 let text_color = universe
407 .world
408 .add_component(ColorComponent::rgba(0.0, 0.0, 0.0, 1.0));
409 universe.add(text_color);
410 let _ = universe.attach(text_id, text_color);
411
412 let state = ButtonState {
414 pressed: false,
415 cap_root,
416 cap_raised_z,
417 cap_pressed_z,
418 frame_color,
419 face_color,
420 text_id,
421 text_color,
422 frame_color_up: color,
423 face_color_up: background_color,
424 text_color_up: [0.0, 0.0, 0.0, 1.0],
425 frame_color_down: darken(color, 0.75),
426 face_color_down: darken(background_color, 0.75),
427 text_color_down: [1.0, 1.0, 1.0, 1.0],
428 };
429
430 BUTTONS
431 .get_or_init(|| Mutex::new(HashMap::new()))
432 .lock()
433 .expect("button state lock")
434 .insert(button_root, state);
435
436 button_root
437}
438
439fn main() {
440 mittens_engine::example_support::ensure_model_assets();
441 utils::logger::init();
442
443 let world = engine::ecs::World::default();
444 let mut universe = engine::Universe::new(world);
445
446 use engine::ecs::component::{
448 AmbientLightComponent, BackgroundColorComponent, Camera3DComponent,
449 DirectionalLightComponent, InputComponent, InputTransformModeComponent, PointerComponent,
450 TransformComponent,
451 };
452
453 let bg = universe
454 .world
455 .add_component(BackgroundColorComponent::new());
456 let bg_c = universe
457 .world
458 .add_component(engine::ecs::component::ColorComponent::rgba(
459 0.92, 0.92, 0.96, 1.0,
460 ));
461 let _ = universe.world.add_child(bg, bg_c);
462 universe.add(bg);
463
464 let ambient = universe
465 .world
466 .add_component(AmbientLightComponent::rgb(0.35, 0.35, 0.35));
467 universe.add(ambient);
468
469 let sun_t = universe
470 .world
471 .add_component(TransformComponent::new().with_position(1.0, 1.0, 1.0));
472 let sun = universe
473 .world
474 .add_component(DirectionalLightComponent::new());
475 let _ = universe.attach(sun_t, sun);
476 universe.add(sun_t);
477
478 let input = universe
479 .world
480 .add_component(InputComponent::new().with_speed(2.5));
481 let input_mode = universe.world.add_component(
482 InputTransformModeComponent::forward_z()
483 .with_fps_rotation()
484 .with_roll_axis_y(),
485 );
486 let _ = universe.attach(input, input_mode);
487
488 let rig_t = universe
489 .world
490 .add_component(TransformComponent::new().with_position(0.0, 0.0, 2.5));
491 let _ = universe.attach(input, rig_t);
492
493 let cam = universe
494 .world
495 .add_component(Camera3DComponent::new().with_far(600.0).with_fov(70.0));
496 let _ = universe.attach(rig_t, cam);
497
498 let pointer = universe.world.add_component(PointerComponent::new());
499 let _ = universe.attach(cam, pointer);
500
501 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_t);
502 universe.add(input);
503
504 let button_root = spawn_button(
506 &mut universe,
507 [0.0, 0.0, 0.0],
508 [1.20, 0.45],
509 ButtonBorder::new(0.025, 0.025, 0.025, 0.025),
510 "click me",
511 0.18,
512 [0.15, 0.15, 0.20, 1.0],
513 [0.85, 0.85, 0.92, 1.0],
514 );
515
516 {
522 use engine::ecs::component::{EditorComponent, TransformComponent};
523
524 let editor_root = universe.world.add_component(EditorComponent::new());
525
526 let stack_root = universe
528 .world
529 .add_component(TransformComponent::new().with_position(-1.55, 0.0, 0.0));
530 let _ = universe.attach(editor_root, stack_root);
531
532 let cube = 0.22_f32;
533 let gap = 0.04_f32;
534 let step = cube + gap;
535 let y_bottom = -0.5 * step;
536 let y_top = 0.5 * step;
537 let x_left = -0.5 * step;
538 let x_right = 0.5 * step;
539
540 let yellow = [1.0, 0.92, 0.22, 1.0];
541 let light_brown = [0.80, 0.66, 0.46, 1.0];
542 let cyan = [0.20, 0.95, 1.0, 1.0];
543
544 let _bottom_left = spawn_raycastable_colored_cube(
545 &mut universe,
546 stack_root,
547 [x_left, y_bottom, 0.0],
548 [cube, cube, 0.06],
549 yellow,
550 );
551 let _bottom_right = spawn_raycastable_colored_cube(
552 &mut universe,
553 stack_root,
554 [x_right, y_bottom, 0.0],
555 [cube, cube, 0.06],
556 light_brown,
557 );
558 let _top = spawn_raycastable_colored_cube(
559 &mut universe,
560 stack_root,
561 [0.0, y_top, 0.0],
562 [cube, cube, 0.06],
563 cyan,
564 );
565
566 universe.add(editor_root);
567 }
568
569 universe.add_signal_handler(
570 engine::ecs::SignalKind::DragStart,
571 button_root,
572 button_press_handler,
573 );
574 universe.add_signal_handler(
575 engine::ecs::SignalKind::DragEnd,
576 button_root,
577 button_press_handler,
578 );
579
580 universe.systems.process_commands(
581 &mut universe.world,
582 &mut universe.visuals,
583 &mut universe.render_assets,
584 &mut universe.command_queue,
585 );
586
587 universe.enable_repl();
588 engine::Windowing::run_app(universe).expect("Windowing failed");
589}