use egui::emath::Numeric;
use egui::{DragValue, Ui};
use crate::editor::gui_elements::{icon_button_sized, EditorTheme};
use crate::editor::icons::paint_editor_icon;
use crate::editor::EditorIcon;
pub fn property_row<R>(
ui: &mut Ui,
label: &str,
add: impl FnOnce(&mut Ui) -> R,
) -> R {
let label_width = (ui.available_width() * 0.4).clamp(64.0, 150.0);
ui.horizontal(|ui| {
let (rect, response) = ui.allocate_exact_size(
egui::vec2(label_width, EditorTheme::ROW_HEIGHT),
egui::Sense::hover(),
);
ui.painter_at(rect.shrink2(egui::vec2(4.0, 0.0))).text(
rect.left_center() + egui::vec2(4.0, 0.0),
egui::Align2::LEFT_CENTER,
label,
egui::TextStyle::Body.resolve(ui.style()),
EditorTheme::TEXT_MUTED,
);
response.on_hover_text(label);
add(ui)
})
.inner
}
fn fill(ui: &mut Ui, widget: impl egui::Widget) -> egui::Response {
ui.add_sized([ui.available_width(), EditorTheme::ROW_HEIGHT], widget)
}
pub fn drag(ui: &mut Ui, label: &str, value: DragValue<'_>) -> bool {
property_row(ui, label, |ui| fill(ui, value).changed())
}
pub fn angle(
ui: &mut Ui,
label: &str,
radians: &mut f32,
degrees: std::ops::RangeInclusive<f32>,
) -> bool {
let mut value = radians.to_degrees();
let changed = drag(
ui,
label,
DragValue::new(&mut value)
.range(degrees)
.speed(0.5)
.suffix("°"),
);
if changed {
*radians = value.to_radians();
}
changed
}
pub fn vec3<N: Numeric>(
ui: &mut Ui,
label: &str,
values: &mut [N; 3],
speed: f64,
) -> bool {
property_row(ui, label, |ui| {
let gap = 2.0;
ui.spacing_mut().item_spacing.x = gap;
let width = ((ui.available_width() - gap * 2.0) / 3.0).max(28.0);
let mut changed = false;
for (axis, value) in values.iter_mut().enumerate() {
let response = ui.add_sized(
[width, EditorTheme::ROW_HEIGHT],
DragValue::new(value).speed(speed).max_decimals(3),
);
changed |= response.changed();
let stripe = egui::Rect::from_min_size(
response.rect.min + egui::vec2(1.0, 4.0),
egui::vec2(2.0, response.rect.height() - 8.0),
);
ui.painter()
.rect_filled(stripe, 1.0, EditorTheme::AXIS[axis]);
}
changed
})
}
pub fn color(ui: &mut Ui, label: &str, rgb: &mut [f32; 3]) -> bool {
property_row(ui, label, |ui| {
ui.spacing_mut().interact_size.x = ui.available_width();
ui.color_edit_button_rgb(rgb).changed()
})
}
pub fn checkbox(ui: &mut Ui, label: &str, value: &mut bool) -> bool {
property_row(ui, label, |ui| ui.checkbox(value, "").changed())
}
pub fn text(ui: &mut Ui, label: &str, value: &mut String) -> bool {
property_row(ui, label, |ui| {
fill(ui, egui::TextEdit::singleline(value)).changed()
})
}
pub fn value(ui: &mut Ui, label: &str, value: &str) {
property_row(ui, label, |ui| {
ui.add(
egui::Label::new(egui::RichText::new(value).monospace()).truncate(),
)
.on_hover_text(value);
});
}
pub fn choice<T: PartialEq + Copy>(
ui: &mut Ui,
label: &str,
value: &mut T,
options: &[(T, &str)],
) -> bool {
let selected = options
.iter()
.find(|(option, _)| option == value)
.map_or("", |(_, name)| name);
property_row(ui, label, |ui| {
let mut changed = false;
egui::ComboBox::from_id_salt(label)
.width(ui.available_width())
.selected_text(selected)
.show_ui(ui, |ui| {
for (option, name) in options {
changed |=
ui.selectable_value(value, *option, *name).changed();
}
});
changed
})
}
pub fn section(
ui: &mut Ui,
title: &str,
removable: bool,
body: impl FnOnce(&mut Ui),
) -> bool {
let id = ui.make_persistent_id(("inspector_section", title));
let mut state =
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
true,
);
let mut remove = false;
egui::Frame::new()
.fill(EditorTheme::PANEL_RAISED)
.corner_radius(EditorTheme::RADIUS)
.inner_margin(egui::Margin::symmetric(2, 1))
.show(ui, |ui| {
ui.horizontal(|ui| {
let remove_width = if removable { 24.0 } else { 0.0 };
let (rect, response) = ui.allocate_exact_size(
egui::vec2(
ui.available_width() - remove_width,
EditorTheme::ROW_HEIGHT - 2.0,
),
egui::Sense::click(),
);
let arrow = egui::Rect::from_center_size(
egui::pos2(rect.left() + 9.0, rect.center().y),
egui::vec2(14.0, 14.0),
);
paint_editor_icon(
ui.painter(),
if state.is_open() {
EditorIcon::ChevronDown
} else {
EditorIcon::ChevronRight
},
arrow,
EditorTheme::TEXT_MUTED,
);
ui.painter().text(
egui::pos2(rect.left() + 20.0, rect.center().y),
egui::Align2::LEFT_CENTER,
title,
egui::FontId::proportional(13.0),
EditorTheme::TEXT,
);
if response.clicked() {
state.toggle(ui);
}
if removable {
remove = icon_button_sized(
ui,
EditorIcon::Close,
false,
true,
egui::vec2(20.0, 18.0),
)
.on_hover_text("Remove component")
.clicked();
}
});
});
state.store(ui.ctx());
state.show_body_unindented(ui, |ui| {
ui.add_space(2.0);
body(ui);
ui.add_space(4.0);
});
remove
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn property_rows_share_one_label_column() {
let context = egui::Context::default();
let mut starts = Vec::new();
let _ = context.run(egui::RawInput::default(), |context| {
egui::CentralPanel::default().show(context, |ui| {
let mut number = 1.0_f32;
let mut values = [0.0_f32; 3];
let mut name = String::new();
starts.push(property_row(ui, "Short", |ui| {
fill(ui, DragValue::new(&mut number)).rect.left()
}));
starts.push(property_row(ui, "A much longer label", |ui| {
ui.available_rect_before_wrap().left()
}));
vec3(ui, "Position", &mut values, 0.1);
text(ui, "Name", &mut name);
});
});
assert_eq!(starts.len(), 2);
assert_eq!(starts[0], starts[1]);
}
}