use crate::gui::actions::Action;
use crate::gui::app::{CellaApp, Dim, DrawMode};
use crate::gui::interact::MAX_BRUSH;
use crate::gui::layers::Layer;
use crate::gui::patterns::PATTERNS;
use crate::gui::theme::section;
use cella_lib::{CellType, INACTIVE, Neighborhood2D};
const BRUSH_SHAPES: [Neighborhood2D; 5] = [
Neighborhood2D::Moore,
Neighborhood2D::VonNeumann,
Neighborhood2D::Langton,
Neighborhood2D::StraightLine,
Neighborhood2D::Knight,
];
pub(in crate::gui) fn neighborhood_name(nb: Neighborhood2D) -> &'static str {
match nb {
Neighborhood2D::Moore => "Moore (square)",
Neighborhood2D::VonNeumann => "Von Neumann (diamond)",
Neighborhood2D::Langton => "Langton (diagonals)",
Neighborhood2D::StraightLine => "Straight lines (cross)",
Neighborhood2D::Knight => "Knight (chess moves)",
}
}
impl CellaApp {
pub(in crate::gui) fn ui_edit_tab(&mut self, ui: &mut egui::Ui) {
let mut pending: Vec<Action> = Vec::new();
let is_2d = matches!(self.scenario.dim, Some(Dim::D2));
section(ui, "Tool", |ui| {
ui.horizontal(|ui| {
let mut tools = vec![
(
DrawMode::Cycle,
"Cycle",
"Click a cell to step it to the next type",
),
(
DrawMode::Paint,
"Paint",
"Hold the left button and drag to paint the chosen type",
),
];
if is_2d {
tools.push((
DrawMode::Stamp,
"Stamp",
"Click to drop a pattern (its top-left corner lands on the cell)",
));
}
for (mode, label, help) in tools {
if ui
.selectable_label(self.edit.draw_mode == mode, label)
.on_hover_text(help)
.clicked()
{
pending.push(Action::SetDrawMode(mode));
}
}
});
if self.edit.draw_mode == DrawMode::Paint {
ui.horizontal(|ui| {
let mut brush = self.edit.brush;
if ui
.add(egui::Slider::new(&mut brush, 0..=MAX_BRUSH).text("Brush range"))
.on_hover_text("0 paints one cell; the outline under the mouse shows the footprint ([ and ] resize)")
.changed()
{
pending.push(Action::SetBrush(brush));
}
});
if is_2d {
ui.horizontal(|ui| {
ui.label("Shape");
let mut shape = self.edit.brush_shape;
egui::ComboBox::from_id_salt("brush_shape")
.selected_text(neighborhood_name(shape))
.show_ui(ui, |ui| {
for nb in BRUSH_SHAPES {
ui.selectable_value(&mut shape, nb, neighborhood_name(nb));
}
});
if shape != self.edit.brush_shape {
pending.push(Action::SetBrushShape(shape));
}
ui.small("same footprints the rules count");
});
}
}
if self.edit.draw_mode == DrawMode::Stamp && is_2d {
let current = PATTERNS.get(self.edit.stamp).map_or("", |p| p.name);
let mut sel = self.edit.stamp;
egui::ComboBox::from_id_salt("stamp_pattern")
.selected_text(current)
.show_ui(ui, |ui| {
for (i, p) in PATTERNS.iter().enumerate() {
ui.selectable_value(&mut sel, i, p.name);
}
});
if sel != self.edit.stamp {
pending.push(Action::SelectStamp(sel));
}
}
ui.horizontal(|ui| {
ui.label("Paint type");
let mut names: Vec<String> = self
.declared_types()
.into_iter()
.map(|t| t.as_str().to_string())
.collect();
names.sort();
names.sort_by_key(|a| a != INACTIVE);
let current = self
.edit
.selected_draw_type
.map(|t| t.as_str().to_string())
.unwrap_or_else(|| INACTIVE.to_string());
let mut sel = current.clone();
egui::ComboBox::from_id_salt("paint_type")
.selected_text(sel.clone())
.show_ui(ui, |ui| {
for n in &names {
ui.selectable_value(&mut sel, n.clone(), n);
}
});
if sel != current {
pending.push(Action::SetDrawType(CellType::from(sel.as_str())));
}
});
ui.small("Editing works while paused. Ctrl+Z undoes the last stroke.");
});
section(ui, "Random fill", |ui| {
ui.add(
egui::Slider::new(&mut self.edit.fill_density, 0.0..=1.0).text("Share of cells"),
);
ui.horizontal(|ui| {
ui.label("Type");
let types: Vec<CellType> = self
.declared_types()
.into_iter()
.filter(|t| *t != CellType::inactive())
.collect();
if self.edit.fill_type.is_none_or(|t| !types.contains(&t)) {
self.edit.fill_type = types.first().copied();
}
let current = self.edit.fill_type.map_or(INACTIVE, |t| t.as_str());
egui::ComboBox::from_id_salt("fill_type")
.selected_text(current)
.show_ui(ui, |ui| {
for t in &types {
ui.selectable_value(&mut self.edit.fill_type, Some(*t), t.as_str());
}
});
});
ui.horizontal(|ui| {
ui.label("Seed");
ui.add(egui::DragValue::new(&mut self.edit.fill_seed));
if ui.button("\u{21BB}").on_hover_text("Next seed").clicked() {
self.edit.fill_seed = self.edit.fill_seed.wrapping_add(1);
}
ui.checkbox(&mut self.edit.fill_clear, "Clear first")
.on_hover_text(
"Empty the grid before filling and make the result the new starting state",
);
});
let can_fill = self.edit.fill_type.is_some() && !self.playback.playing;
if ui
.add_enabled(can_fill, egui::Button::new("Fill"))
.on_hover_text("Reproducible: the same seed always paints the same picture")
.clicked()
&& let Some(ty) = self.edit.fill_type
{
pending.push(Action::RandomFill {
density: self.edit.fill_density,
ty,
seed: self.edit.fill_seed,
clear_first: self.edit.fill_clear,
});
}
});
section(ui, "Fun", |ui| {
let paused = !self.playback.playing;
let has_grid = self.scenario.dim.is_some();
ui.horizontal_wrapped(|ui| {
if ui
.add_enabled(paused && has_grid, egui::Button::new("Surprise me"))
.on_hover_text(
"Roll every knob of the rule and model at random, then fill the grid \
30 % with the first type. Uses the seed above; Undo rule takes it back.",
)
.clicked()
{
pending.push(Action::SurpriseMeDraft);
}
if ui
.add_enabled(paused && has_grid, egui::Button::new("Mutate rule"))
.on_hover_text("Nudge every knob a little from where it is now")
.clicked()
{
pending.push(Action::MutateRuleDraft);
}
if ui
.add_enabled(
paused && !self.edit.rule_undo.is_empty(),
egui::Button::new("Undo rule"),
)
.on_hover_text(
"Put the knobs back the way they were before the last change (Ctrl+U)",
)
.clicked()
{
pending.push(Action::UndoRule);
}
});
ui.add(
egui::Slider::new(&mut self.edit.mutate_sigma, 0.01..=1.0)
.logarithmic(true)
.text("Mutation size"),
)
.on_hover_text("How far a mutation moves each knob, as a share of its range");
ui.small("R fills, Shift+R surprises, M mutates; each press moves the seed on.");
});
section(ui, "Layers", |ui| {
let mut lines = self.view.show_grid_lines;
if ui.checkbox(&mut lines, "Grid lines").changed() {
pending.push(Action::ToggleLayer(Layer::Grid));
}
let mut age = self.view.layers.age;
if ui
.checkbox(&mut age, "Age heat")
.on_hover_text("Tint cells by how recently they changed: fronts glow")
.changed()
{
pending.push(Action::ToggleLayer(Layer::Age));
}
if self.view.layers.age {
let mut cap = self.view.layers.age_cap;
ui.horizontal(|ui| {
ui.label("Fade after");
if ui
.add(egui::DragValue::new(&mut cap).range(1..=100_000))
.changed()
{
pending.push(Action::SetAgeCap(cap));
}
ui.label("steps");
});
}
let has_map = self.view.layers.probability_map.is_some();
let mut prob = self.view.layers.probability;
if ui
.add_enabled(has_map, egui::Checkbox::new(&mut prob, "Probability map"))
.on_hover_text(
"The Explore ensemble's per-cell probability (needs a running ensemble)",
)
.changed()
{
pending.push(Action::ToggleLayer(Layer::Probability));
}
let mut opacity = self.view.layers.opacity;
if ui
.add(egui::Slider::new(&mut opacity, 0.0..=1.0).text("Opacity"))
.changed()
{
pending.push(Action::SetLayerOpacity(opacity));
}
});
section(ui, "Export GIF", |ui| {
ui.label("Steps, frame rate and 1D row stacking are asked for after you pick a file.");
let exporting = self.export.join.is_some();
if ui
.add_enabled(!exporting, egui::Button::new("Export GIF\u{2026}"))
.clicked()
{
pending.push(Action::ExportGif);
}
if let Some((done, total)) = self.export_progress() {
ui.add(crate::gui::panels::toolbar::export_progress_bar(done, total));
}
if let Some(msg) = &self.export.message {
ui.label(msg.clone());
}
});
for a in pending {
self.push(a);
}
}
}