// PropertyEditor - Modern property editing component for game editors
// Supports numeric values with labels, units, drag-to-adjust, and constraints
use super::traits::Renderable
// Property types for different value editors
pub enum PropertyType {
Number { min: f32, max: f32, step: f32 },
Integer { min: i32, max: i32 },
Boolean,
Text,
Color,
Dropdown { options: Vec<string> },
}
// A single property row
pub struct Property {
pub name: string,
pub value: string,
pub property_type: PropertyType,
pub unit: string,
pub tooltip: string,
pub on_change: string,
}
impl Property {
pub fn number(name: string, value: f32, min: f32, max: f32) -> Property {
Property {
name,
value: format!("{:.3}", value),
property_type: PropertyType::Number { min: min, max: max, step: 0.1 },
unit: "".to_string(),
tooltip: "".to_string(),
on_change: "".to_string(),
}
}
pub fn integer(name: string, value: i32, min: i32, max: i32) -> Property {
Property {
name,
value: format!("{}", value),
property_type: PropertyType::Integer { min: min, max: max },
unit: "".to_string(),
tooltip: "".to_string(),
on_change: "".to_string(),
}
}
pub fn boolean(name: string, value: bool) -> Property {
Property {
name,
value: if value { "true".to_string() } else { "false".to_string() },
property_type: PropertyType::Boolean,
unit: "".to_string(),
tooltip: "".to_string(),
on_change: "".to_string(),
}
}
pub fn text(name: string, value: string) -> Property {
Property {
name,
value,
property_type: PropertyType::Text,
unit: "".to_string(),
tooltip: "".to_string(),
on_change: "".to_string(),
}
}
pub fn color(name: string, value: string) -> Property {
Property {
name,
value,
property_type: PropertyType::Color,
unit: "".to_string(),
tooltip: "".to_string(),
on_change: "".to_string(),
}
}
pub fn unit(self, unit: string) -> Property {
self.unit = unit
self
}
pub fn tooltip(self, tooltip: string) -> Property {
self.tooltip = tooltip
self
}
pub fn on_change(self, handler: string) -> Property {
self.on_change = handler
self
}
}
impl Renderable for Property {
pub fn render(self) -> string {
let tooltip_attr = if self.tooltip != "" {
format!(" title='{}'", self.tooltip)
} else {
"".to_string()
}
let unit_html = if self.unit != "" {
format!("<span class='prop-unit'>{}</span>", self.unit)
} else {
"".to_string()
}
let input_html = match self.property_type {
PropertyType::Number { min: mn, max: mx, step: st } => {
format!("
<div class='prop-number'>
<input type='number' class='prop-input'
value='{}' min='{}' max='{}' step='{}'
onchange='{}(this.value)'/>
{}
</div>
", self.value, mn, mx, st, self.on_change, unit_html)
},
PropertyType::Integer { min: mn, max: mx } => {
format!("
<div class='prop-number'>
<input type='number' class='prop-input'
value='{}' min='{}' max='{}' step='1'
onchange='{}(this.value)'/>
{}
</div>
", self.value, mn, mx, self.on_change, unit_html)
},
PropertyType::Boolean => {
let checked = if self.value == "true" { "checked" } else { "" }
format!("
<label class='prop-toggle'>
<input type='checkbox' {} onchange='{}(this.checked)'/>
<span class='toggle-slider'></span>
</label>
", checked, self.on_change)
},
PropertyType::Text => {
format!("
<input type='text' class='prop-input prop-text'
value='{}' onchange='{}(this.value)'/>
", self.value, self.on_change)
},
PropertyType::Color => {
format!("
<div class='prop-color'>
<input type='color' class='color-swatch'
value='{}' onchange='{}(this.value)'/>
<input type='text' class='color-hex'
value='{}' onchange='{}(this.value)'/>
</div>
", self.value, self.on_change, self.value, self.on_change)
},
PropertyType::Dropdown { options: opts } => {
let mut options_html = "".to_string()
for o in opts {
let selected = if o.as_str() == self.value.as_str() { "selected" } else { "" }
options_html = options_html + format!("<option value='{}' {}>{}</option>", o, selected, o).as_str()
}
format!("
<select class='prop-select' onchange='{}(this.value)'>
{}
</select>
", self.on_change, options_html)
},
}
format!("
<div class='prop-row'{}>
<label class='prop-label'>{}</label>
<div class='prop-value'>
{}
</div>
</div>
", tooltip_attr, self.name, input_html)
}
}
// Vec3 Editor for 3D vectors
pub struct Vec3Editor {
pub label: string,
pub x: f32,
pub y: f32,
pub z: f32,
pub on_change: string,
}
impl Vec3Editor {
pub fn new(label: string, x: f32, y: f32, z: f32) -> Vec3Editor {
Vec3Editor { label, x, y, z, on_change: "".to_string() }
}
pub fn on_change(self, handler: string) -> Vec3Editor {
self.on_change = handler
self
}
}
impl Renderable for Vec3Editor {
pub fn render(self) -> string {
format!("
<div class='vec3-editor'>
<label class='prop-label'>{}</label>
<div class='vec3-inputs'>
<div class='vec3-axis'>
<span class='axis-label x'>X</span>
<input type='number' step='0.1' value='{:.3}'
onchange='{}(\"x\", this.value)'/>
</div>
<div class='vec3-axis'>
<span class='axis-label y'>Y</span>
<input type='number' step='0.1' value='{:.3}'
onchange='{}(\"y\", this.value)'/>
</div>
<div class='vec3-axis'>
<span class='axis-label z'>Z</span>
<input type='number' step='0.1' value='{:.3}'
onchange='{}(\"z\", this.value)'/>
</div>
</div>
</div>
", self.label, self.x, self.on_change, self.y, self.on_change, self.z, self.on_change)
}
}
// CSS styles for property editors
pub fn property_editor_styles() -> string {
"
.prop-row {
display: flex;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.prop-row:hover {
background: rgba(255,255,255,0.02);
}
.prop-label {
width: 100px;
font-size: 12px;
color: #999;
flex-shrink: 0;
}
.prop-value {
flex: 1;
}
.prop-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #333;
border-radius: 4px;
background: #1a1a2e;
color: #e0e0e0;
font-size: 12px;
}
.prop-input:focus {
border-color: #e94560;
outline: none;
box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.2);
}
.prop-number {
display: flex;
align-items: center;
gap: 4px;
}
.prop-unit {
font-size: 11px;
color: #666;
}
/* Toggle switch */
.prop-toggle {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.prop-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #333;
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: '';
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.prop-toggle input:checked + .toggle-slider {
background-color: #e94560;
}
.prop-toggle input:checked + .toggle-slider:before {
transform: translateX(20px);
}
/* Color editor */
.prop-color {
display: flex;
gap: 8px;
align-items: center;
}
.color-swatch {
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.color-hex {
width: 80px;
padding: 6px 8px;
border: 1px solid #333;
border-radius: 4px;
background: #1a1a2e;
color: #e0e0e0;
font-family: monospace;
font-size: 12px;
}
/* Vec3 editor */
.vec3-editor {
display: flex;
align-items: center;
padding: 6px 0;
}
.vec3-inputs {
display: flex;
gap: 4px;
flex: 1;
}
.vec3-axis {
display: flex;
align-items: center;
flex: 1;
}
.vec3-axis input {
width: 100%;
padding: 6px 8px;
border: 1px solid #333;
border-radius: 0 4px 4px 0;
background: #1a1a2e;
color: #e0e0e0;
font-size: 12px;
}
.axis-label {
padding: 6px 8px;
font-size: 11px;
font-weight: 600;
border-radius: 4px 0 0 4px;
}
.axis-label.x { background: #e94560; color: white; }
.axis-label.y { background: #4ade80; color: #1a1a2e; }
.axis-label.z { background: #60a5fa; color: white; }
/* Select dropdown */
.prop-select {
width: 100%;
padding: 6px 10px;
border: 1px solid #333;
border-radius: 4px;
background: #1a1a2e;
color: #e0e0e0;
font-size: 12px;
cursor: pointer;
}
".to_string()
}