bevy_yoleck/
auto_edit.rs

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/// Attributes that can be applied to fields for customizing their UI
15#[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    /// Auto-edit with field-level attributes (label, tooltip, range, etc.)
30    /// Default implementation wraps auto_edit with label and common decorations
31    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    /// Internal implementation for field rendering with label
47    /// Types can override this to customize behavior based on attributes
48    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        // Get entity info map once for both display and drag&drop validation
549        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        // Handle drag & drop
599        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    // Populate entity display info in egui context only if component has entity ref fields
639    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}