use egui::{Color32, Context, DragValue, Slider, Ui, Vec2};
use super::super::shared::{ParticleLayer, begin_body, settings_window};
use super::LightMotion;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct LightSettings {
pub open: bool,
pub active: bool,
pub color: Color32,
pub density: usize,
pub radius_min: f32,
pub radius_max: f32,
pub intensity: f32,
pub motion: LightMotion,
pub wind: Vec2,
pub flicker: f32,
pub lifetime_min: f32,
pub lifetime_max: f32,
pub layer: ParticleLayer,
}
impl Default for LightSettings {
fn default() -> Self {
Self {
open: false,
active: true,
color: Color32::from_rgb(255, 224, 178),
density: 48,
radius_min: 8.0,
radius_max: 36.0,
intensity: 0.7,
motion: LightMotion::Drift,
wind: Vec2::ZERO,
flicker: 0.25,
lifetime_min: 4.0,
lifetime_max: 9.0,
layer: ParticleLayer::Foreground,
}
}
}
impl LightSettings {
pub fn reset(&mut self) {
let open = self.open;
*self = Self::default();
self.open = open;
}
pub fn show_window(&mut self, ctx: &Context) -> Option<egui::Response> {
let mut open = self.open;
let resp = settings_window(ctx, "Light Settings", &mut open, |ui| {
self.body_ui(ui)
});
self.open = open;
resp
}
pub fn body_ui(&mut self, ui: &mut Ui) {
let theme = begin_body(ui);
ui.heading("General");
ui.checkbox(&mut self.active, "Effect Active");
ui.horizontal(|ui| {
ui.label("Color:");
ui.color_edit_button_srgba(&mut self.color);
});
ui.separator();
ui.heading("Motion");
ui.horizontal(|ui| {
ui.label("Motion:");
ui.selectable_value(
&mut self.motion,
LightMotion::Drift,
LightMotion::Drift.to_str(),
)
.on_hover_text("Slow wandering motes that bounce off the area edges");
ui.selectable_value(
&mut self.motion,
LightMotion::Rise,
LightMotion::Rise.to_str(),
)
.on_hover_text("Embers that rise from the bottom edge and sway as they climb");
});
ui.separator();
ui.heading("Particles");
ui.horizontal(|ui| {
ui.label("Density (Count):");
ui.add(Slider::new(&mut self.density, 0..=2000).logarithmic(true));
});
ui.horizontal(|ui| {
ui.label("Radius (px):");
ui.add(
DragValue::new(&mut self.radius_min)
.speed(0.5)
.range(1.0..=200.0)
.prefix("Min: "),
);
ui.add(
DragValue::new(&mut self.radius_max)
.speed(0.5)
.range(1.0..=200.0)
.prefix("Max: "),
);
});
if self.radius_min > self.radius_max {
self.radius_min = self.radius_max;
}
ui.horizontal(|ui| {
ui.label("Intensity:");
ui.add(Slider::new(&mut self.intensity, 0.0..=1.0));
});
ui.horizontal(|ui| {
ui.label("Flicker:");
ui.add(Slider::new(&mut self.flicker, 0.0..=1.0));
});
ui.separator();
ui.heading("Physics");
ui.label("Wind Vector:");
ui.horizontal(|ui| {
ui.add(Slider::new(&mut self.wind.x, -200.0..=200.0).text("Horizontal"));
});
ui.horizontal(|ui| {
ui.add(Slider::new(&mut self.wind.y, -100.0..=100.0).text("Vertical"));
});
ui.horizontal(|ui| {
ui.label("Lifetime (s):");
ui.add(
DragValue::new(&mut self.lifetime_min)
.speed(0.1)
.range(0.5..=60.0)
.prefix("Min: "),
);
ui.add(
DragValue::new(&mut self.lifetime_max)
.speed(0.1)
.range(0.5..=60.0)
.prefix("Max: "),
);
});
if self.lifetime_min > self.lifetime_max {
self.lifetime_min = self.lifetime_max;
}
ui.separator();
ui.heading("Rendering");
ui.horizontal(|ui| {
ui.label("Layer:");
ui.selectable_value(
&mut self.layer,
ParticleLayer::Background,
ParticleLayer::Background.to_str(),
)
.on_hover_text("Behind everything (Order::Background)");
ui.selectable_value(
&mut self.layer,
ParticleLayer::Foreground,
ParticleLayer::Foreground.to_str(),
)
.on_hover_text("In front of windows (Order::Foreground)");
ui.selectable_value(
&mut self.layer,
ParticleLayer::Top,
ParticleLayer::Top.to_str(),
)
.on_hover_text("On top of everything including debug/tooltips (Order::Debug)");
});
ui.add_space(theme.spacing.sm);
if ui.button("Reset Defaults").clicked() {
self.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::__run_test_ctx;
#[test]
fn defaults_are_sane() {
let settings = LightSettings::default();
assert!(!settings.open);
assert!(settings.active);
assert!(settings.density > 0);
assert!(settings.radius_min <= settings.radius_max);
assert!(settings.lifetime_min <= settings.lifetime_max);
assert!(
settings.lifetime_min > 1.0,
"lifetimes are multi-second"
);
assert_eq!(settings.motion, LightMotion::Drift);
assert_eq!(settings.layer, ParticleLayer::Foreground);
}
#[test]
fn reset_keeps_open() {
let mut settings = LightSettings {
open: true,
active: false,
density: 3,
radius_min: 90.0,
radius_max: 91.0,
intensity: 0.1,
motion: LightMotion::Rise,
wind: Vec2::new(7.0, 8.0),
flicker: 0.9,
lifetime_min: 1.0,
lifetime_max: 2.0,
layer: ParticleLayer::Top,
color: Color32::from_rgb(9, 9, 9),
};
settings.reset();
assert!(settings.open, "reset must not close the window");
assert_eq!(
settings,
LightSettings {
open: true,
..Default::default()
}
);
}
fn window_height_on(screen: Vec2) -> f32 {
let ctx = Context::default();
let mut theme = crate::theme::Theme::new(crate::theme::ThemeKind::TokyoNight);
theme.install(&ctx);
let mut settings = LightSettings {
open: true,
..Default::default()
};
let mut size = Vec2::ZERO;
for _ in 0..2 {
let output = ctx.run_ui(
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
screen,
)),
..Default::default()
},
|ui| {
if let Some(resp) = settings.show_window(ui.ctx()) {
size = resp.rect.size();
}
},
);
output.drop_without_applying_deltas();
}
size.y
}
#[test]
fn settings_body_fits_the_demo_root() {
let height = window_height_on(Vec2::new(900.0, 2000.0));
assert!(
height <= 650.0,
"settings window is {height} tall, root is 650"
);
}
#[test]
fn body_ui_renders_without_a_window() {
let mut settings = LightSettings::default();
assert!(!settings.open);
egui::__run_test_ui(|ui| {
settings.body_ui(ui);
});
assert!(
!settings.open,
"body_ui must not touch the window flag"
);
}
#[test]
fn show_window_returns_response_only_when_open() {
__run_test_ctx(|ctx| {
let mut closed = LightSettings::default();
assert!(closed.show_window(ctx).is_none());
});
__run_test_ctx(|ctx| {
let mut open = LightSettings {
open: true,
..Default::default()
};
assert!(open.show_window(ctx).is_some());
});
}
}