use bevy_egui::egui;
use bevy_map_animation::SpriteData;
use bevy_map_core::{
ComponentOverrides, EntityTypeConfig, InputConfig, InputOverrides, PhysicsConfig,
PhysicsOverrides, SpriteConfig, SpriteOverrides,
};
use uuid::Uuid;
use crate::project::Project;
use crate::EditorState;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Selection {
#[default]
None,
Level(Uuid),
Layer(Uuid, usize), Entity(Uuid, Uuid), Tileset(Uuid),
DataType(String), DataInstance(Uuid),
SpriteSheet(Uuid), Dialogue(String), MultipleDataInstances(Vec<Uuid>),
MultipleEntities(Vec<(Uuid, Uuid)>), }
#[derive(Default)]
pub struct InspectorResult {
pub delete_data_instance: Option<Uuid>,
pub delete_entity: Option<(Uuid, Uuid)>,
pub open_sprite_editor: Option<(String, Uuid)>,
pub open_dialogue_editor: Option<(String, Uuid)>,
pub edit_sprite_sheet: Option<Uuid>,
pub edit_sprite_sheet_settings: Option<Uuid>,
pub edit_dialogue: Option<String>,
pub create_instance_for_array: Option<(String, Uuid, String)>,
}
pub fn render_inspector(
ui: &mut egui::Ui,
editor_state: &mut EditorState,
project: &mut Project,
) -> InspectorResult {
let mut result = InspectorResult::default();
ui.heading("Inspector");
ui.separator();
match &editor_state.selection {
Selection::None => {
ui.label("Nothing selected");
}
Selection::Level(level_id) => {
render_level_inspector(ui, *level_id, project);
}
Selection::Layer(level_id, layer_idx) => {
render_layer_inspector(ui, *level_id, *layer_idx, project);
}
Selection::Entity(level_id, entity_id) => {
if render_entity_inspector(ui, *level_id, *entity_id, project) {
result.delete_entity = Some((*level_id, *entity_id));
}
}
Selection::Tileset(tileset_id) => {
render_tileset_inspector(ui, *tileset_id, project);
}
Selection::DataType(type_name) => {
render_data_type_inspector(ui, type_name, project);
}
Selection::DataInstance(instance_id) => {
if render_data_instance_inspector(ui, *instance_id, project, &mut result) {
result.delete_data_instance = Some(*instance_id);
}
}
Selection::SpriteSheet(sprite_sheet_id) => {
render_sprite_sheet_inspector(ui, *sprite_sheet_id, project, &mut result);
}
Selection::Dialogue(ref dialogue_id) => {
render_dialogue_inspector(ui, dialogue_id, project, &mut result);
}
Selection::MultipleDataInstances(ids) => {
ui.label(format!("{} data instances selected", ids.len()));
ui.label("Use context menu for bulk operations");
}
Selection::MultipleEntities(items) => {
ui.label(format!("{} entities selected", items.len()));
ui.label("Use context menu for bulk operations");
}
}
result
}
fn render_level_inspector(ui: &mut egui::Ui, level_id: Uuid, project: &mut Project) {
let Some(level) = project.get_level_mut(level_id) else {
ui.label("Level not found");
return;
};
ui.label(format!("Level: {}", level.name));
ui.separator();
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut level.name);
});
ui.horizontal(|ui| {
ui.label("Size:");
ui.label(format!("{}x{}", level.width, level.height));
});
ui.label(format!("Layers: {}", level.layers.len()));
ui.label(format!("Entities: {}", level.entities.len()));
}
fn render_layer_inspector(
ui: &mut egui::Ui,
level_id: Uuid,
layer_idx: usize,
project: &mut Project,
) {
let Some(level) = project.get_level_mut(level_id) else {
ui.label("Level not found");
return;
};
let Some(layer) = level.layers.get_mut(layer_idx) else {
ui.label("Layer not found");
return;
};
ui.label(format!("Layer: {}", layer.name));
ui.separator();
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut layer.name);
});
ui.horizontal(|ui| {
ui.label("Visible:");
ui.checkbox(&mut layer.visible, "");
});
ui.horizontal(|ui| {
ui.label("Opacity:");
ui.add(egui::Slider::new(&mut layer.opacity, 0.0..=1.0));
});
}
fn render_entity_inspector(
ui: &mut egui::Ui,
level_id: Uuid,
entity_id: Uuid,
project: &mut Project,
) -> bool {
let mut should_delete = false;
let (
type_name,
type_def,
entity_type_config,
enums,
sprite_sheets,
dialogue_options,
ref_options,
animation_names,
) = {
let Some(level) = project.get_level(level_id) else {
ui.label("Level not found");
return false;
};
let Some(entity) = level.get_entity(entity_id) else {
ui.label("Entity not found");
return false;
};
let type_name = entity.type_name.clone();
let type_def = project.schema.get_type(&type_name).cloned();
let entity_type_config = project.get_entity_type_config(&type_name).cloned();
let enums = project.schema.enums.clone();
let sprite_sheets: Vec<SpriteData> = project.sprite_sheets.clone();
let dialogue_options: Vec<(String, String)> = project
.dialogues
.iter()
.map(|d| (d.id.clone(), d.name.clone()))
.collect();
let ref_options: std::collections::HashMap<String, Vec<(String, String)>> = project
.data
.instances
.iter()
.map(|(type_name, instances)| {
let opts: Vec<(String, String)> = instances
.iter()
.map(|inst| {
let name = inst
.properties
.get("name")
.and_then(|v| v.as_string())
.unwrap_or(&inst.id.to_string())
.to_string();
(inst.id.to_string(), name)
})
.collect();
(type_name.clone(), opts)
})
.collect();
let animation_names: Vec<String> = entity_type_config
.as_ref()
.and_then(|cfg| cfg.sprite.as_ref())
.and_then(|sprite_cfg| sprite_cfg.sprite_sheet_id)
.and_then(|sheet_id| project.get_sprite_sheet(sheet_id))
.map(|sheet| sheet.animations.keys().cloned().collect())
.unwrap_or_default();
(
type_name,
type_def,
entity_type_config,
enums,
sprite_sheets,
dialogue_options,
ref_options,
animation_names,
)
};
let Some(level) = project.get_level_mut(level_id) else {
ui.label("Level not found");
return false;
};
let Some(entity) = level.get_entity_mut(entity_id) else {
ui.label("Entity not found");
return false;
};
ui.label(format!("Entity: {}", type_name));
ui.separator();
ui.horizontal(|ui| {
ui.label("Position:");
ui.add(
egui::DragValue::new(&mut entity.position[0])
.speed(1.0)
.prefix("X: "),
);
ui.add(
egui::DragValue::new(&mut entity.position[1])
.speed(1.0)
.prefix("Y: "),
);
});
if let Some(type_def) = type_def {
ui.separator();
ui.label("Properties");
for prop_def in &type_def.properties {
if !should_show_property(prop_def, &entity.properties) {
continue;
}
if !entity.properties.contains_key(&prop_def.name) {
entity
.properties
.insert(prop_def.name.clone(), get_default_value(prop_def));
}
let value = entity.properties.get_mut(&prop_def.name).unwrap();
let id_salt = format!("entity_{}_{}", entity_id, prop_def.name);
ui.horizontal(|ui| {
ui.label(&prop_def.name);
if prop_def.required {
ui.colored_label(egui::Color32::RED, "*");
}
});
render_property_value_editor(
ui,
prop_def,
value,
&id_salt,
&enums,
&sprite_sheets,
&dialogue_options,
&ref_options,
);
}
}
if let Some(ref type_config) = entity_type_config {
render_component_overrides_section(
ui,
entity_id,
&mut entity.component_overrides,
type_config,
&animation_names,
);
}
ui.separator();
if ui.button("Delete Entity").clicked() {
should_delete = true;
}
should_delete
}
fn render_tileset_inspector(ui: &mut egui::Ui, tileset_id: Uuid, project: &mut Project) {
let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) else {
ui.label("Tileset not found");
return;
};
ui.label(format!("Tileset: {}", tileset.name));
ui.separator();
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut tileset.name);
});
ui.horizontal(|ui| {
ui.label("Tile Size:");
ui.add(egui::DragValue::new(&mut tileset.tile_size).range(1..=256));
});
ui.label(format!("Images: {}", tileset.images.len()));
ui.label(format!("Total Tiles: {}", tileset.total_tile_count()));
}
fn render_data_type_inspector(ui: &mut egui::Ui, type_name: &str, project: &mut Project) {
let Some(type_def) = project.schema.get_type(type_name) else {
ui.label("Type not found");
return;
};
ui.label(format!("Data Type: {}", type_name));
ui.separator();
ui.horizontal(|ui| {
ui.label("Color:");
let hex = type_def.color.trim_start_matches('#');
if hex.len() >= 6 {
if let (Ok(r), Ok(g), Ok(b)) = (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
) {
let color = egui::Color32::from_rgb(r, g, b);
let (rect, _) =
ui.allocate_exact_size(egui::vec2(50.0, 20.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, color);
ui.label(format!("#{}", hex));
}
}
});
ui.horizontal(|ui| {
ui.label("Placeable:");
ui.label(if type_def.placeable { "Yes" } else { "No" });
});
if let Some(icon) = &type_def.icon {
ui.horizontal(|ui| {
ui.label("Icon:");
ui.label(icon);
});
}
ui.separator();
ui.label(format!("Properties ({}):", type_def.properties.len()));
for prop in &type_def.properties {
ui.horizontal(|ui| {
ui.label(&prop.name);
ui.label(format!("({:?})", prop.prop_type));
if prop.required {
ui.label("*required");
}
});
}
ui.separator();
let instance_count = project
.data
.instances
.get(type_name)
.map(|v| v.len())
.unwrap_or(0);
ui.label(format!("Instances: {}", instance_count));
}
fn render_data_instance_inspector(
ui: &mut egui::Ui,
instance_id: Uuid,
project: &mut Project,
result: &mut InspectorResult,
) -> bool {
let mut should_delete = false;
let (type_name, type_def, enums, sprite_sheets, dialogue_options, ref_options) = {
let Some(instance) = project.get_data_instance(instance_id) else {
ui.label("Instance not found");
return false;
};
let type_name = instance.type_name.clone();
let type_def = project.schema.get_type(&type_name).cloned();
let enums = project.schema.enums.clone();
let sprite_sheets: Vec<SpriteData> = project.sprite_sheets.clone();
let dialogue_options: Vec<(String, String)> = project
.dialogues
.iter()
.map(|d| (d.id.clone(), d.name.clone()))
.collect();
let ref_options: std::collections::HashMap<String, Vec<(String, String)>> = project
.data
.instances
.iter()
.map(|(type_name, instances)| {
let opts: Vec<(String, String)> = instances
.iter()
.map(|inst| {
let name = inst
.properties
.get("name")
.and_then(|v| v.as_string())
.unwrap_or(&inst.id.to_string())
.to_string();
(inst.id.to_string(), name)
})
.collect();
(type_name.clone(), opts)
})
.collect();
(
type_name,
type_def,
enums,
sprite_sheets,
dialogue_options,
ref_options,
)
};
let Some(instance) = project.get_data_instance_mut(instance_id) else {
ui.label("Instance not found");
return false;
};
ui.label(format!("Data Instance: {}", type_name));
ui.separator();
if let Some(type_def) = type_def {
for prop_def in &type_def.properties {
if !should_show_property(prop_def, &instance.properties) {
continue;
}
if !instance.properties.contains_key(&prop_def.name) {
instance
.properties
.insert(prop_def.name.clone(), get_default_value(prop_def));
}
let value = instance.properties.get_mut(&prop_def.name).unwrap();
let id_salt = format!("data_instance_{}_{}", instance_id, prop_def.name);
ui.horizontal(|ui| {
ui.label(&prop_def.name);
if prop_def.required {
ui.colored_label(egui::Color32::RED, "*");
}
});
if let Some(create_type) = render_property_value_editor(
ui,
prop_def,
value,
&id_salt,
&enums,
&sprite_sheets,
&dialogue_options,
&ref_options,
) {
result.create_instance_for_array =
Some((create_type, instance_id, prop_def.name.clone()));
}
}
} else {
ui.label("(No schema found for this type)");
for (key, value) in instance.properties.iter() {
ui.horizontal(|ui| {
ui.label(format!("{}:", key));
ui.label(format!("{:?}", value));
});
}
}
ui.separator();
if ui.button("Delete Instance").clicked() {
should_delete = true;
}
should_delete
}
fn render_sprite_sheet_inspector(
ui: &mut egui::Ui,
sprite_sheet_id: Uuid,
project: &mut Project,
result: &mut InspectorResult,
) {
if let Some(sprite_sheet) = project.get_sprite_sheet_mut(sprite_sheet_id) {
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut sprite_sheet.name);
});
}
let Some(sprite_sheet) = project.get_sprite_sheet(sprite_sheet_id) else {
ui.label("Sprite Sheet not found");
return;
};
ui.separator();
ui.horizontal(|ui| {
ui.label("Sheet:");
ui.label(if sprite_sheet.sheet_path.is_empty() {
"(not set)"
} else {
&sprite_sheet.sheet_path
});
});
ui.horizontal(|ui| {
ui.label("Frame size:");
ui.label(format!(
"{}x{}",
sprite_sheet.frame_width, sprite_sheet.frame_height
));
});
ui.horizontal(|ui| {
ui.label("Grid:");
ui.label(format!(
"{} columns x {} rows",
sprite_sheet.columns, sprite_sheet.rows
));
});
ui.horizontal(|ui| {
ui.label("Total frames:");
ui.label(format!("{}", sprite_sheet.total_frames()));
});
ui.horizontal(|ui| {
ui.label("Animations:");
ui.label(format!("{}", sprite_sheet.animations.len()));
});
ui.separator();
if ui.button("Edit Animations").clicked() {
result.edit_sprite_sheet = Some(sprite_sheet_id);
}
if ui.button("Edit Sheet").clicked() {
result.edit_sprite_sheet_settings = Some(sprite_sheet_id);
}
}
fn render_dialogue_inspector(
ui: &mut egui::Ui,
dialogue_id: &str,
project: &mut Project,
result: &mut InspectorResult,
) {
if let Some(dialogue) = project.get_dialogue_mut(dialogue_id) {
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut dialogue.name);
});
}
let Some(dialogue) = project.get_dialogue(dialogue_id) else {
ui.label("Dialogue not found");
return;
};
ui.separator();
ui.horizontal(|ui| {
ui.label("ID:");
ui.label(&dialogue.id);
});
ui.horizontal(|ui| {
ui.label("Nodes:");
ui.label(format!("{}", dialogue.nodes.len()));
});
if !dialogue.start_node.is_empty() {
ui.horizontal(|ui| {
ui.label("Start node:");
ui.label(&dialogue.start_node);
});
}
ui.separator();
if ui.button("Open Editor").clicked() {
result.edit_dialogue = Some(dialogue_id.to_string());
}
}
#[allow(deprecated)] pub fn get_default_value(prop_def: &bevy_map_schema::PropertyDef) -> bevy_map_core::Value {
use bevy_map_core::Value;
use bevy_map_schema::PropType;
if let Some(default) = &prop_def.default {
return Value::from_json(default.clone());
}
match prop_def.prop_type {
PropType::String | PropType::Multiline => Value::String(String::new()),
PropType::Int => Value::Int(0),
PropType::Float => Value::Float(0.0),
PropType::Bool => Value::Bool(false),
PropType::Enum => Value::String(String::new()),
PropType::Ref => Value::Null,
PropType::Array => Value::Array(Vec::new()),
PropType::Point => Value::Object(
[
("x".to_string(), Value::Float(0.0)),
("y".to_string(), Value::Float(0.0)),
]
.into_iter()
.collect(),
),
PropType::Color => Value::String("#808080".to_string()),
PropType::Sprite => Value::Null,
PropType::Dialogue => Value::Null,
PropType::Embedded => Value::Null,
}
}
fn should_show_property(
prop_def: &bevy_map_schema::PropertyDef,
properties: &std::collections::HashMap<String, bevy_map_core::Value>,
) -> bool {
let Some(show_if) = &prop_def.show_if else {
return true;
};
if let Some((prop_name, expected)) = show_if.split_once('=') {
if let Some(actual) = properties.get(prop_name.trim()) {
let actual_str = match actual {
bevy_map_core::Value::String(s) => s.clone(),
bevy_map_core::Value::Bool(b) => b.to_string(),
bevy_map_core::Value::Int(i) => i.to_string(),
_ => return false,
};
return actual_str == expected.trim();
}
return false;
}
true
}
fn parse_hex_color_to_rgb(hex: &str) -> [f32; 3] {
let hex = hex.trim_start_matches('#');
if hex.len() >= 6 {
if let (Ok(r), Ok(g), Ok(b)) = (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
) {
return [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0];
}
}
[0.5, 0.5, 0.5]
}
#[allow(clippy::too_many_arguments)]
#[allow(deprecated)] fn render_property_value_editor(
ui: &mut egui::Ui,
prop_def: &bevy_map_schema::PropertyDef,
value: &mut bevy_map_core::Value,
id_salt: &str,
enums: &std::collections::HashMap<String, Vec<String>>,
sprite_sheets: &[SpriteData],
dialogue_options: &[(String, String)],
ref_options: &std::collections::HashMap<String, Vec<(String, String)>>,
) -> Option<String> {
use bevy_map_core::Value;
use bevy_map_schema::PropType;
match prop_def.prop_type {
PropType::String => {
let mut s = value.as_string().unwrap_or(&String::new()).to_string();
if ui.text_edit_singleline(&mut s).changed() {
*value = Value::String(s);
}
}
PropType::Multiline => {
let mut s = value.as_string().unwrap_or(&String::new()).to_string();
if ui.text_edit_multiline(&mut s).changed() {
*value = Value::String(s);
}
}
PropType::Int => {
let mut i = value.as_int().unwrap_or(0);
let mut drag = egui::DragValue::new(&mut i);
let min_val = prop_def.min.map(|m| m as i64).unwrap_or(i64::MIN);
let max_val = prop_def.max.map(|m| m as i64).unwrap_or(i64::MAX);
drag = drag.range(min_val..=max_val);
if ui.add(drag).changed() {
*value = Value::Int(i);
}
}
PropType::Float => {
let mut f = value.as_float().unwrap_or(0.0);
let mut drag = egui::DragValue::new(&mut f).speed(0.1);
let min_val = prop_def.min.unwrap_or(f64::MIN);
let max_val = prop_def.max.unwrap_or(f64::MAX);
drag = drag.range(min_val..=max_val);
if ui.add(drag).changed() {
*value = Value::Float(f);
}
}
PropType::Bool => {
let mut b = value.as_bool().unwrap_or(false);
if ui.checkbox(&mut b, "").changed() {
*value = Value::Bool(b);
}
}
PropType::Enum => {
if let Some(enum_type) = &prop_def.enum_type {
if let Some(enum_values) = enums.get(enum_type) {
let current = value.as_string().unwrap_or(&String::new()).to_string();
egui::ComboBox::from_id_salt(id_salt)
.selected_text(if current.is_empty() {
"(None)"
} else {
¤t
})
.show_ui(ui, |ui| {
for enum_val in enum_values {
if ui
.selectable_label(current == *enum_val, enum_val)
.clicked()
{
*value = Value::String(enum_val.clone());
}
}
});
}
}
}
PropType::Ref => {
if let Some(ref_type) = &prop_def.ref_type {
let current_id = value.as_string().unwrap_or(&String::new()).to_string();
let instances = ref_options.get(ref_type);
let current_name = instances
.and_then(|opts| opts.iter().find(|(id, _)| *id == current_id))
.map(|(_, name)| name.as_str())
.unwrap_or("(None)");
egui::ComboBox::from_id_salt(id_salt)
.selected_text(current_name)
.show_ui(ui, |ui| {
if ui
.selectable_label(current_id.is_empty(), "(None)")
.clicked()
{
*value = Value::Null;
}
if let Some(opts) = instances {
for (id, name) in opts {
if ui.selectable_label(*id == current_id, name).clicked() {
*value = Value::String(id.clone());
}
}
}
});
}
}
PropType::Point => {
let (mut x, mut y) = match value {
Value::Object(obj) => (
obj.get("x").and_then(|v| v.as_float()).unwrap_or(0.0),
obj.get("y").and_then(|v| v.as_float()).unwrap_or(0.0),
),
_ => (0.0, 0.0),
};
let mut changed = false;
ui.horizontal(|ui| {
changed |= ui
.add(egui::DragValue::new(&mut x).speed(1.0).prefix("X: "))
.changed();
changed |= ui
.add(egui::DragValue::new(&mut y).speed(1.0).prefix("Y: "))
.changed();
});
if changed {
*value = Value::Object(
[
("x".to_string(), Value::Float(x)),
("y".to_string(), Value::Float(y)),
]
.into_iter()
.collect(),
);
}
}
PropType::Color => {
let current = value
.as_string()
.unwrap_or(&"#808080".to_string())
.to_string();
let mut rgb = parse_hex_color_to_rgb(¤t);
if ui.color_edit_button_rgb(&mut rgb).changed() {
*value = Value::String(format!(
"#{:02x}{:02x}{:02x}",
(rgb[0] * 255.0) as u8,
(rgb[1] * 255.0) as u8,
(rgb[2] * 255.0) as u8
));
}
}
PropType::Sprite => {
let current_id = match value {
Value::Object(obj) => obj
.get("id")
.and_then(|v| v.as_string())
.map(|s| s.to_string()),
_ => None,
}
.unwrap_or_default();
let current_name = sprite_sheets
.iter()
.find(|s| s.id.to_string() == current_id)
.map(|s| s.name.as_str())
.unwrap_or("(None)");
egui::ComboBox::from_id_salt(id_salt)
.selected_text(current_name)
.show_ui(ui, |ui| {
if ui
.selectable_label(current_id.is_empty(), "(None)")
.clicked()
{
*value = Value::Null;
}
for sprite_data in sprite_sheets {
let id_str = sprite_data.id.to_string();
if ui
.selectable_label(id_str == current_id, &sprite_data.name)
.clicked()
{
if let Ok(json) = serde_json::to_value(sprite_data) {
*value = Value::from_json(json);
}
}
}
});
}
PropType::Dialogue => {
let current_id = value.as_string().unwrap_or(&String::new()).to_string();
let current_name = dialogue_options
.iter()
.find(|(id, _)| *id == current_id)
.map(|(_, name)| name.as_str())
.unwrap_or("(None)");
egui::ComboBox::from_id_salt(id_salt)
.selected_text(current_name)
.show_ui(ui, |ui| {
if ui
.selectable_label(current_id.is_empty(), "(None)")
.clicked()
{
*value = Value::Null;
}
for (id, name) in dialogue_options {
if ui.selectable_label(*id == current_id, name).clicked() {
*value = Value::String(id.clone());
}
}
});
}
PropType::Array => {
return render_array_editor(ui, prop_def, value, id_salt, ref_options);
}
PropType::Embedded => {
ui.label("(embedded type)");
}
}
None
}
fn render_array_editor(
ui: &mut egui::Ui,
prop_def: &bevy_map_schema::PropertyDef,
value: &mut bevy_map_core::Value,
id_salt: &str,
ref_options: &std::collections::HashMap<String, Vec<(String, String)>>,
) -> Option<String> {
use bevy_map_core::Value;
if !matches!(value, Value::Array(_)) {
*value = Value::Array(Vec::new());
}
let item_type = prop_def.item_type.as_deref().unwrap_or("String");
let is_custom_type = ref_options.contains_key(item_type);
let instances = if is_custom_type {
ref_options.get(item_type)
} else {
None
};
let Value::Array(items) = value else {
return None;
};
let item_count = items.len();
let mut create_new_type: Option<String> = None;
let header_text = if let Some(ref item_type_name) = prop_def.item_type {
format!(
"{}: Array<{}> ({} items)",
prop_def.name, item_type_name, item_count
)
} else {
format!("{} ({} items)", prop_def.name, item_count)
};
egui::CollapsingHeader::new(header_text)
.id_salt(id_salt)
.default_open(item_count < 5)
.show(ui, |ui| {
let mut to_remove = None;
for (idx, item) in items.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.label(format!("[{}]", idx));
if is_custom_type {
let current_id = item.as_string().unwrap_or(&String::new()).to_string();
let current_name = instances
.and_then(|opts| opts.iter().find(|(id, _)| *id == current_id))
.map(|(_, name)| name.as_str())
.unwrap_or("(None)");
egui::ComboBox::from_id_salt(format!("{}_{}", id_salt, idx))
.selected_text(current_name)
.show_ui(ui, |ui| {
if ui
.selectable_label(current_id.is_empty(), "(None)")
.clicked()
{
*item = Value::Null;
}
if let Some(opts) = instances {
for (id, name) in opts {
if ui.selectable_label(*id == current_id, name).clicked() {
*item = Value::String(id.clone());
}
}
}
});
} else {
match item_type {
"String" => {
let mut s = item.as_string().unwrap_or(&String::new()).to_string();
if ui.text_edit_singleline(&mut s).changed() {
*item = Value::String(s);
}
}
"Int" => {
let mut i = item.as_int().unwrap_or(0);
if ui.add(egui::DragValue::new(&mut i)).changed() {
*item = Value::Int(i);
}
}
"Float" => {
let mut f = item.as_float().unwrap_or(0.0);
if ui.add(egui::DragValue::new(&mut f).speed(0.1)).changed() {
*item = Value::Float(f);
}
}
"Bool" => {
let mut b = item.as_bool().unwrap_or(false);
if ui.checkbox(&mut b, "").changed() {
*item = Value::Bool(b);
}
}
_ => {
let mut s = item.as_string().unwrap_or(&String::new()).to_string();
if ui.text_edit_singleline(&mut s).changed() {
*item = Value::String(s);
}
}
}
}
if ui.small_button("X").clicked() {
to_remove = Some(idx);
}
});
}
if let Some(idx) = to_remove {
items.remove(idx);
}
if is_custom_type {
ui.horizontal(|ui| {
if ui.button("+ Add Existing").clicked() {
items.push(Value::Null);
}
if ui.button(format!("+ Create New {}", item_type)).clicked() {
create_new_type = Some(item_type.to_string());
}
});
} else {
if ui.button("+ Add").clicked() {
let new_item = match item_type {
"String" => Value::String(String::new()),
"Int" => Value::Int(0),
"Float" => Value::Float(0.0),
"Bool" => Value::Bool(false),
_ => Value::String(String::new()),
};
items.push(new_item);
}
}
});
create_new_type
}
fn render_component_overrides_section(
ui: &mut egui::Ui,
entity_id: Uuid,
overrides: &mut ComponentOverrides,
type_config: &EntityTypeConfig,
animation_names: &[String],
) {
let has_physics = type_config.physics.is_some();
let has_input = type_config.input.is_some();
let has_sprite = type_config.sprite.is_some();
if !has_physics && !has_input && !has_sprite {
return;
}
ui.separator();
let header_id = format!("components_{}", entity_id);
egui::CollapsingHeader::new("Components")
.id_salt(&header_id)
.default_open(true)
.show(ui, |ui| {
if let Some(ref physics_config) = type_config.physics {
render_physics_overrides(ui, entity_id, overrides, physics_config);
}
if let Some(ref input_config) = type_config.input {
render_input_overrides(ui, entity_id, overrides, input_config);
}
if let Some(ref sprite_config) = type_config.sprite {
render_sprite_overrides(ui, entity_id, overrides, sprite_config, animation_names);
}
ui.add_space(8.0);
if !overrides.is_empty() {
if ui.button("Reset to Type Defaults").clicked() {
overrides.clear();
}
}
});
}
fn render_physics_overrides(
ui: &mut egui::Ui,
entity_id: Uuid,
overrides: &mut ComponentOverrides,
physics_config: &PhysicsConfig,
) {
let physics_id = format!("physics_{}", entity_id);
egui::CollapsingHeader::new("Physics")
.id_salt(&physics_id)
.default_open(false)
.show(ui, |ui| {
let physics = overrides
.physics
.get_or_insert_with(PhysicsOverrides::default);
render_override_field_f32(
ui,
"Gravity Scale",
&format!("{}_gravity", physics_id),
&mut physics.gravity_scale,
physics_config.gravity_scale,
0.01,
);
render_override_field_f32(
ui,
"Friction",
&format!("{}_friction", physics_id),
&mut physics.friction,
physics_config.friction,
0.01,
);
render_override_field_f32(
ui,
"Restitution",
&format!("{}_restitution", physics_id),
&mut physics.restitution,
physics_config.restitution,
0.01,
);
render_override_field_f32(
ui,
"Linear Damping",
&format!("{}_linear_damping", physics_id),
&mut physics.linear_damping,
physics_config.linear_damping,
0.1,
);
if physics.is_empty() {
overrides.physics = None;
}
});
}
fn render_input_overrides(
ui: &mut egui::Ui,
entity_id: Uuid,
overrides: &mut ComponentOverrides,
input_config: &InputConfig,
) {
let input_id = format!("input_{}", entity_id);
egui::CollapsingHeader::new("Input")
.id_salt(&input_id)
.default_open(false)
.show(ui, |ui| {
let input = overrides.input.get_or_insert_with(InputOverrides::default);
render_override_field_f32(
ui,
"Speed",
&format!("{}_speed", input_id),
&mut input.speed,
input_config.speed,
1.0,
);
if let Some(default_jump) = input_config.jump_force {
render_override_field_f32_opt(
ui,
"Jump Force",
&format!("{}_jump", input_id),
&mut input.jump_force,
default_jump,
1.0,
);
}
render_override_field_f32(
ui,
"Acceleration",
&format!("{}_accel", input_id),
&mut input.acceleration,
input_config.acceleration,
1.0,
);
render_override_field_f32(
ui,
"Deceleration",
&format!("{}_decel", input_id),
&mut input.deceleration,
input_config.deceleration,
1.0,
);
if let Some(default_fall) = input_config.max_fall_speed {
render_override_field_f32_opt(
ui,
"Max Fall Speed",
&format!("{}_fall", input_id),
&mut input.max_fall_speed,
default_fall,
1.0,
);
}
if input.is_empty() {
overrides.input = None;
}
});
}
fn render_sprite_overrides(
ui: &mut egui::Ui,
entity_id: Uuid,
overrides: &mut ComponentOverrides,
sprite_config: &SpriteConfig,
animation_names: &[String],
) {
let sprite_id = format!("sprite_{}", entity_id);
egui::CollapsingHeader::new("Sprite")
.id_salt(&sprite_id)
.default_open(false)
.show(ui, |ui| {
let sprite = overrides
.sprite
.get_or_insert_with(SpriteOverrides::default);
let default_scale = sprite_config.scale.unwrap_or(1.0);
render_override_field_f32_opt(
ui,
"Scale",
&format!("{}_scale", sprite_id),
&mut sprite.scale,
default_scale,
0.1,
);
let default_anim = sprite_config.default_animation.as_deref().unwrap_or("idle");
ui.horizontal(|ui| {
ui.label("Animation:");
let current = sprite.default_animation.as_deref().unwrap_or(default_anim);
let is_overridden = sprite.default_animation.is_some();
if is_overridden {
ui.colored_label(egui::Color32::YELLOW, "*");
}
egui::ComboBox::from_id_salt(format!("{}_anim", sprite_id))
.selected_text(current)
.show_ui(ui, |ui| {
if ui
.selectable_label(
!is_overridden,
format!("(default: {})", default_anim),
)
.clicked()
{
sprite.default_animation = None;
}
for anim_name in animation_names {
let selected = is_overridden
&& sprite.default_animation.as_deref() == Some(anim_name);
if ui.selectable_label(selected, anim_name).clicked() {
sprite.default_animation = Some(anim_name.clone());
}
}
});
if !is_overridden {
ui.label(
egui::RichText::new(format!("(def: {})", default_anim))
.weak()
.small(),
);
}
});
if sprite.is_empty() {
overrides.sprite = None;
}
});
}
fn render_override_field_f32(
ui: &mut egui::Ui,
label: &str,
id: &str,
value: &mut Option<f32>,
default: f32,
speed: f32,
) {
ui.horizontal(|ui| {
ui.label(format!("{}:", label));
let is_overridden = value.is_some();
let current = value.unwrap_or(default);
if is_overridden {
ui.colored_label(egui::Color32::YELLOW, "*");
}
let mut edit_val = current;
let response = ui
.push_id(id, |ui| {
ui.add(
egui::DragValue::new(&mut edit_val)
.speed(speed)
.min_decimals(1)
.max_decimals(2),
)
})
.inner;
if response.changed() {
*value = Some(edit_val);
}
ui.label(
egui::RichText::new(format!("(def: {:.1})", default))
.weak()
.small(),
);
if is_overridden {
if ui
.small_button("↺")
.on_hover_text("Reset to default")
.clicked()
{
*value = None;
}
}
});
}
fn render_override_field_f32_opt(
ui: &mut egui::Ui,
label: &str,
id: &str,
value: &mut Option<f32>,
default: f32,
speed: f32,
) {
render_override_field_f32(ui, label, id, value, default, speed);
}