1use bevy::prelude::*;
2use bevy_egui::egui;
3
4use crate::YoleckInternalSchedule;
5use crate::entity_ref::resolve_entity_refs;
6
7use crate::entity_ref::validate_entity_ref_requirements_for;
8
9use crate::entity_ref::YoleckEntityRef;
10use crate::prelude::YoleckUuidRegistry;
11
12use std::collections::HashMap;
13
14#[derive(Default, Clone)]
16pub struct FieldAttrs {
17 pub label: Option<String>,
18 pub tooltip: Option<String>,
19 pub range: Option<(f64, f64)>,
20 pub speed: Option<f64>,
21 pub readonly: bool,
22 pub multiline: bool,
23 pub entity_filter: Option<String>,
24}
25
26pub trait YoleckAutoEdit: Send + Sync + 'static {
27 fn auto_edit(value: &mut Self, ui: &mut egui::Ui);
28
29 fn auto_edit_with_label_and_attrs(
32 value: &mut Self,
33 ui: &mut egui::Ui,
34 label: &str,
35 attrs: &FieldAttrs,
36 ) {
37 if attrs.readonly {
38 ui.add_enabled_ui(false, |ui| {
39 Self::auto_edit_field_impl(value, ui, label, attrs);
40 });
41 } else {
42 Self::auto_edit_field_impl(value, ui, label, attrs);
43 }
44 }
45
46 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
49 ui.horizontal(|ui| {
50 ui.label(label);
51 let response = ui
52 .scope(|ui| {
53 Self::auto_edit(value, ui);
54 })
55 .response;
56
57 if let Some(tooltip) = &attrs.tooltip {
58 response.on_hover_text(tooltip);
59 }
60 });
61 }
62}
63
64pub fn render_auto_edit_value<T: YoleckAutoEdit>(ui: &mut egui::Ui, value: &mut T) {
65 T::auto_edit(value, ui);
66}
67
68impl YoleckAutoEdit for f32 {
69 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
70 ui.add(egui::DragValue::new(value).speed(0.1));
71 }
72
73 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
74 ui.horizontal(|ui| {
75 ui.label(label);
76 let response = if let Some((min, max)) = attrs.range {
77 ui.add(egui::Slider::new(value, min as f32..=max as f32))
78 } else {
79 let speed = attrs.speed.unwrap_or(0.1) as f32;
80 ui.add(egui::DragValue::new(value).speed(speed))
81 };
82
83 if let Some(tooltip) = &attrs.tooltip {
84 response.on_hover_text(tooltip);
85 }
86 });
87 }
88}
89
90impl YoleckAutoEdit for f64 {
91 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
92 ui.add(egui::DragValue::new(value).speed(0.1));
93 }
94
95 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
96 ui.horizontal(|ui| {
97 ui.label(label);
98 let response = if let Some((min, max)) = attrs.range {
99 ui.add(egui::Slider::new(value, min..=max))
100 } else {
101 let speed = attrs.speed.unwrap_or(0.1);
102 ui.add(egui::DragValue::new(value).speed(speed))
103 };
104
105 if let Some(tooltip) = &attrs.tooltip {
106 response.on_hover_text(tooltip);
107 }
108 });
109 }
110}
111
112macro_rules! impl_auto_edit_for_integer {
113 ($($ty:ty),*) => {
114 $(
115 impl YoleckAutoEdit for $ty {
116 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
117 ui.add(egui::DragValue::new(value).speed(1.0));
118 }
119
120 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
121 ui.horizontal(|ui| {
122 ui.label(label);
123 let response = if let Some((min, max)) = attrs.range {
124 ui.add(egui::Slider::new(value, min as $ty..=max as $ty))
125 } else {
126 let speed = attrs.speed.unwrap_or(1.0) as f32;
127 ui.add(egui::DragValue::new(value).speed(speed))
128 };
129
130 if let Some(tooltip) = &attrs.tooltip {
131 response.on_hover_text(tooltip);
132 }
133 });
134 }
135 }
136 )*
137 };
138}
139
140impl_auto_edit_for_integer!(i32, i64, u32, u64, usize, isize);
141
142impl YoleckAutoEdit for bool {
143 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
144 ui.checkbox(value, "");
145 }
146
147 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
148 ui.horizontal(|ui| {
149 let response = ui.checkbox(value, label);
150
151 if let Some(tooltip) = &attrs.tooltip {
152 response.on_hover_text(tooltip);
153 }
154 });
155 }
156}
157
158impl YoleckAutoEdit for String {
159 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
160 ui.text_edit_singleline(value);
161 }
162
163 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
164 if attrs.multiline {
165 ui.label(label);
166 let response = ui.text_edit_multiline(value);
167
168 if let Some(tooltip) = &attrs.tooltip {
169 response.on_hover_text(tooltip);
170 }
171 } else {
172 ui.horizontal(|ui| {
173 ui.label(label);
174 let response = ui.text_edit_singleline(value);
175
176 if let Some(tooltip) = &attrs.tooltip {
177 response.on_hover_text(tooltip);
178 }
179 });
180 }
181 }
182}
183
184impl YoleckAutoEdit for Vec2 {
185 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
186 ui.horizontal(|ui| {
187 ui.add(egui::DragValue::new(&mut value.x).prefix("x: ").speed(0.1));
188 ui.add(egui::DragValue::new(&mut value.y).prefix("y: ").speed(0.1));
189 });
190 }
191
192 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
193 let speed = attrs.speed.unwrap_or(0.1) as f32;
194 let response = ui
195 .horizontal(|ui| {
196 ui.label(label);
197 ui.add(
198 egui::DragValue::new(&mut value.x)
199 .prefix("x: ")
200 .speed(speed),
201 );
202 ui.add(
203 egui::DragValue::new(&mut value.y)
204 .prefix("y: ")
205 .speed(speed),
206 );
207 })
208 .response;
209
210 if let Some(tooltip) = &attrs.tooltip {
211 response.on_hover_text(tooltip);
212 }
213 }
214}
215
216impl YoleckAutoEdit for Vec3 {
217 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
218 ui.horizontal(|ui| {
219 ui.add(egui::DragValue::new(&mut value.x).prefix("x: ").speed(0.1));
220 ui.add(egui::DragValue::new(&mut value.y).prefix("y: ").speed(0.1));
221 ui.add(egui::DragValue::new(&mut value.z).prefix("z: ").speed(0.1));
222 });
223 }
224
225 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
226 let speed = attrs.speed.unwrap_or(0.1) as f32;
227 let response = ui
228 .horizontal(|ui| {
229 ui.label(label);
230 ui.add(
231 egui::DragValue::new(&mut value.x)
232 .prefix("x: ")
233 .speed(speed),
234 );
235 ui.add(
236 egui::DragValue::new(&mut value.y)
237 .prefix("y: ")
238 .speed(speed),
239 );
240 ui.add(
241 egui::DragValue::new(&mut value.z)
242 .prefix("z: ")
243 .speed(speed),
244 );
245 })
246 .response;
247
248 if let Some(tooltip) = &attrs.tooltip {
249 response.on_hover_text(tooltip);
250 }
251 }
252}
253
254impl YoleckAutoEdit for Vec4 {
255 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
256 ui.horizontal(|ui| {
257 ui.add(egui::DragValue::new(&mut value.x).prefix("x: ").speed(0.1));
258 ui.add(egui::DragValue::new(&mut value.y).prefix("y: ").speed(0.1));
259 ui.add(egui::DragValue::new(&mut value.z).prefix("z: ").speed(0.1));
260 ui.add(egui::DragValue::new(&mut value.w).prefix("w: ").speed(0.1));
261 });
262 }
263
264 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
265 let speed = attrs.speed.unwrap_or(0.1) as f32;
266 let response = ui
267 .horizontal(|ui| {
268 ui.label(label);
269 ui.add(
270 egui::DragValue::new(&mut value.x)
271 .prefix("x: ")
272 .speed(speed),
273 );
274 ui.add(
275 egui::DragValue::new(&mut value.y)
276 .prefix("y: ")
277 .speed(speed),
278 );
279 ui.add(
280 egui::DragValue::new(&mut value.z)
281 .prefix("z: ")
282 .speed(speed),
283 );
284 ui.add(
285 egui::DragValue::new(&mut value.w)
286 .prefix("w: ")
287 .speed(speed),
288 );
289 })
290 .response;
291
292 if let Some(tooltip) = &attrs.tooltip {
293 response.on_hover_text(tooltip);
294 }
295 }
296}
297
298impl YoleckAutoEdit for Quat {
299 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
300 let (mut yaw, mut pitch, mut roll) = value.to_euler(EulerRot::YXZ);
301 yaw = yaw.to_degrees();
302 pitch = pitch.to_degrees();
303 roll = roll.to_degrees();
304
305 ui.horizontal(|ui| {
306 let mut changed = false;
307 changed |= ui
308 .add(
309 egui::DragValue::new(&mut yaw)
310 .prefix("yaw: ")
311 .speed(1.0)
312 .suffix("°"),
313 )
314 .changed();
315 changed |= ui
316 .add(
317 egui::DragValue::new(&mut pitch)
318 .prefix("pitch: ")
319 .speed(1.0)
320 .suffix("°"),
321 )
322 .changed();
323 changed |= ui
324 .add(
325 egui::DragValue::new(&mut roll)
326 .prefix("roll: ")
327 .speed(1.0)
328 .suffix("°"),
329 )
330 .changed();
331
332 if changed {
333 *value = Quat::from_euler(
334 EulerRot::YXZ,
335 yaw.to_radians(),
336 pitch.to_radians(),
337 roll.to_radians(),
338 );
339 }
340 });
341 }
342
343 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
344 let speed = attrs.speed.unwrap_or(1.0) as f32;
345 let response = ui
346 .horizontal(|ui| {
347 ui.label(label);
348 let (mut yaw, mut pitch, mut roll) = value.to_euler(EulerRot::YXZ);
349 yaw = yaw.to_degrees();
350 pitch = pitch.to_degrees();
351 roll = roll.to_degrees();
352
353 let mut changed = false;
354 changed |= ui
355 .add(
356 egui::DragValue::new(&mut yaw)
357 .prefix("yaw: ")
358 .speed(speed)
359 .suffix("°"),
360 )
361 .changed();
362 changed |= ui
363 .add(
364 egui::DragValue::new(&mut pitch)
365 .prefix("pitch: ")
366 .speed(speed)
367 .suffix("°"),
368 )
369 .changed();
370 changed |= ui
371 .add(
372 egui::DragValue::new(&mut roll)
373 .prefix("roll: ")
374 .speed(speed)
375 .suffix("°"),
376 )
377 .changed();
378
379 if changed {
380 *value = Quat::from_euler(
381 EulerRot::YXZ,
382 yaw.to_radians(),
383 pitch.to_radians(),
384 roll.to_radians(),
385 );
386 }
387 })
388 .response;
389
390 if let Some(tooltip) = &attrs.tooltip {
391 response.on_hover_text(tooltip);
392 }
393 }
394}
395
396impl YoleckAutoEdit for Color {
397 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
398 let srgba = value.to_srgba();
399 let mut color_arr = [srgba.red, srgba.green, srgba.blue, srgba.alpha];
400 if ui
401 .color_edit_button_rgba_unmultiplied(&mut color_arr)
402 .changed()
403 {
404 *value = Color::srgba(color_arr[0], color_arr[1], color_arr[2], color_arr[3]);
405 }
406 }
407
408 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
409 let response = ui
410 .horizontal(|ui| {
411 ui.label(label);
412 let srgba = value.to_srgba();
413 let mut color_arr = [srgba.red, srgba.green, srgba.blue, srgba.alpha];
414 if ui
415 .color_edit_button_rgba_unmultiplied(&mut color_arr)
416 .changed()
417 {
418 *value = Color::srgba(color_arr[0], color_arr[1], color_arr[2], color_arr[3]);
419 }
420 })
421 .response;
422
423 if let Some(tooltip) = &attrs.tooltip {
424 response.on_hover_text(tooltip);
425 }
426 }
427}
428
429impl<T: YoleckAutoEdit + Default> YoleckAutoEdit for Option<T> {
430 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
431 ui.horizontal(|ui| {
432 let mut has_value = value.is_some();
433 if ui.checkbox(&mut has_value, "").changed() {
434 if has_value {
435 *value = Some(T::default());
436 } else {
437 *value = None;
438 }
439 }
440 if let Some(inner) = value.as_mut() {
441 T::auto_edit(inner, ui);
442 }
443 });
444 }
445
446 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
447 let response = ui
448 .horizontal(|ui| {
449 ui.label(label);
450 let mut has_value = value.is_some();
451 if ui.checkbox(&mut has_value, "").changed() {
452 if has_value {
453 *value = Some(T::default());
454 } else {
455 *value = None;
456 }
457 }
458 if let Some(inner) = value.as_mut() {
459 T::auto_edit(inner, ui);
460 }
461 })
462 .response;
463
464 if let Some(tooltip) = &attrs.tooltip {
465 response.on_hover_text(tooltip);
466 }
467 }
468}
469
470impl<T: YoleckAutoEdit + Default> YoleckAutoEdit for Vec<T> {
471 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
472 let mut to_remove = None;
473 for (idx, item) in value.iter_mut().enumerate() {
474 ui.horizontal(|ui| {
475 ui.label(format!("[{}]", idx));
476 T::auto_edit(item, ui);
477 if ui.small_button("−").clicked() {
478 to_remove = Some(idx);
479 }
480 });
481 }
482 if let Some(idx) = to_remove {
483 value.remove(idx);
484 }
485 if ui.small_button("+").clicked() {
486 value.push(T::default());
487 }
488 }
489
490 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
491 let response = ui.collapsing(label, |ui| {
492 let mut to_remove = None;
493 for (idx, item) in value.iter_mut().enumerate() {
494 ui.horizontal(|ui| {
495 ui.label(format!("[{}]", idx));
496 T::auto_edit(item, ui);
497 if ui.small_button("−").clicked() {
498 to_remove = Some(idx);
499 }
500 });
501 }
502 if let Some(idx) = to_remove {
503 value.remove(idx);
504 }
505 if ui.small_button("+").clicked() {
506 value.push(T::default());
507 }
508 });
509
510 if let Some(tooltip) = &attrs.tooltip {
511 response.header_response.on_hover_text(tooltip);
512 }
513 }
514}
515
516impl<T: YoleckAutoEdit> YoleckAutoEdit for [T] {
517 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
518 for (idx, item) in value.iter_mut().enumerate() {
519 ui.horizontal(|ui| {
520 ui.label(format!("[{}]", idx));
521 T::auto_edit(item, ui);
522 });
523 }
524 }
525}
526
527#[derive(Clone)]
528struct EntityRefDisplayInfo {
529 pub type_name: String,
530 pub name: String,
531}
532
533impl YoleckAutoEdit for YoleckEntityRef {
534 fn auto_edit(value: &mut Self, ui: &mut egui::Ui) {
535 ui.horizontal(|ui| {
536 if let Some(uuid) = value.uuid() {
537 ui.label(uuid.to_string());
538 if ui.small_button("✕").clicked() {
539 value.clear();
540 }
541 } else {
542 ui.label("None");
543 }
544 });
545 }
546
547 fn auto_edit_field_impl(value: &mut Self, ui: &mut egui::Ui, label: &str, attrs: &FieldAttrs) {
548 let entity_info_map = ui.ctx().data(|data| {
550 data.get_temp::<HashMap<uuid::Uuid, EntityRefDisplayInfo>>(egui::Id::new(
551 "yoleck_entity_ref_display_info",
552 ))
553 });
554
555 let response = ui
556 .horizontal(|ui| {
557 ui.label(label);
558
559 let display_text = if let Some(uuid) = value.uuid() {
560 if let Some(ref info_map) = entity_info_map {
561 if let Some(info) = info_map.get(&uuid) {
562 if info.name.is_empty() {
563 let uuid_str = uuid.to_string();
564 let uuid_short = &uuid_str[..uuid_str.len().min(8)];
565 format!("{} ({})", info.type_name, uuid_short)
566 } else {
567 format!("{} - {}", info.type_name, info.name)
568 }
569 } else {
570 uuid.to_string()
571 }
572 } else {
573 uuid.to_string()
574 }
575 } else {
576 "None".to_string()
577 };
578
579 ui.add(
580 egui::Button::new(
581 egui::RichText::new(display_text)
582 .text_style(ui.style().drag_value_text_style.clone()),
583 )
584 .wrap_mode(egui::TextWrapMode::Extend)
585 .min_size(ui.spacing().interact_size),
586 );
587
588 if value.is_some() && ui.small_button("✕").clicked() {
589 value.clear();
590 }
591
592 if let Some(tooltip) = &attrs.tooltip {
593 ui.label("ⓘ").on_hover_text(tooltip);
594 }
595 })
596 .response;
597
598 if let Some(dropped_uuid) = response.dnd_release_payload::<uuid::Uuid>() {
600 let dropped_uuid = *dropped_uuid;
601
602 let should_accept = if let Some(filter) = &attrs.entity_filter {
603 entity_info_map
604 .as_ref()
605 .and_then(|map| map.get(&dropped_uuid))
606 .is_none_or(|info| &info.type_name == filter)
607 } else {
608 true
609 };
610
611 if should_accept {
612 value.set(dropped_uuid);
613 }
614 }
615 }
616}
617
618use crate::YoleckExtForApp;
619use crate::editing::{YoleckEdit, YoleckUi};
620use crate::specs_registration::YoleckComponent;
621
622use crate::entity_ref::YoleckEntityRefAccessor;
623use bevy::ecs::component::Mutable;
624
625use crate::YoleckManaged;
626use crate::entity_uuid::YoleckEntityUuid;
627
628pub fn auto_edit_system<T: YoleckComponent + YoleckAutoEdit + YoleckEntityRefAccessor>(
629 mut ui: ResMut<YoleckUi>,
630 mut edit: YoleckEdit<&mut T>,
631 entities_query: Query<(&YoleckEntityUuid, &YoleckManaged)>,
632 registry: Res<YoleckUuidRegistry>,
633) {
634 let Ok(mut component) = edit.single_mut() else {
635 return;
636 };
637
638 if !T::entity_ref_fields().is_empty() {
640 let entity_count = entities_query.iter().len();
641 let mut entity_info_map = HashMap::with_capacity(entity_count);
642
643 for (entity_uuid, managed) in entities_query.iter() {
644 entity_info_map.insert(
645 entity_uuid.get(),
646 EntityRefDisplayInfo {
647 type_name: managed.type_name.clone(),
648 name: managed.name.clone(),
649 },
650 );
651 }
652
653 ui.ctx().data_mut(|data| {
654 data.insert_temp(
655 egui::Id::new("yoleck_entity_ref_display_info"),
656 entity_info_map,
657 );
658 });
659 }
660
661 ui.group(|ui| {
662 ui.label(egui::RichText::new(T::KEY).strong());
663 ui.separator();
664 T::auto_edit(&mut component, ui);
665 });
666
667 component.resolve_entity_refs(registry.as_ref());
668}
669
670pub trait YoleckAutoEditExt {
671 fn add_yoleck_auto_edit<
672 T: Component<Mutability = Mutable>
673 + YoleckComponent
674 + YoleckAutoEdit
675 + YoleckEntityRefAccessor,
676 >(
677 &mut self,
678 );
679}
680
681impl YoleckAutoEditExt for App {
682 fn add_yoleck_auto_edit<
683 T: Component<Mutability = Mutable>
684 + YoleckComponent
685 + YoleckAutoEdit
686 + YoleckEntityRefAccessor,
687 >(
688 &mut self,
689 ) {
690 self.add_yoleck_edit_system(auto_edit_system::<T>);
691 self.add_systems(
692 YoleckInternalSchedule::PostLoadResolutions,
693 resolve_entity_refs::<T>,
694 );
695
696 let construction_specs = self
697 .world_mut()
698 .get_resource::<crate::YoleckEntityConstructionSpecs>();
699
700 if let Some(specs) = construction_specs {
701 validate_entity_ref_requirements_for::<T>(specs);
702 }
703 }
704}