1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
//! The rule editor panel: declare cell types, then build and apply the
//! subrule chain that drives the automaton.
use super::rule_edit_model::{Rule1DEdit, Rule1DSubruleEdit, Rule2DEdit, Rule2DSubruleEdit};
use crate::gui::actions::Action;
use crate::gui::app::{CellaApp, Dim};
use crate::gui::panels::widgets::{randomness_control, type_combo};
use cella_lib::*;
impl CellaApp {
pub(in crate::gui) fn ui_rule_editor(&mut self, ui: &mut egui::Ui) {
let mut pending: Vec<Action> = Vec::new();
{
// Manage known types
ui.label("Types/states available to rules:").on_hover_text("Declare the distinct cell states used by your rules. 'Inactive' is reserved and always present.");
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut self.editor.new_type_name)
.on_hover_text("Enter a new state name (e.g., Alive, Dead, A, B). Avoid using 'Inactive'.");
if ui.button("Add type")
.on_hover_text("Add the typed state so it can be used in rules and colored in the viewport.")
.clicked() {
let name = self.editor.new_type_name.trim().to_string();
if !name.is_empty() {
pending.push(Action::AddType(name));
self.editor.new_type_name.clear();
}
}
});
// Show current list (already ordered Inactive-first by `declared_types`)
let names: Vec<&'static str> = self.declared_types().iter().map(|t| t.as_str()).collect();
ui.horizontal_wrapped(|ui| {
for n in &names { ui.label(egui::RichText::new(*n).monospace()); }
});
ui.separator();
if let Some(dim) = self.scenario.dim {
match dim {
Dim::D1 => {
// Ensure editor model exists
if self.editor.rule_1d.is_none()
&& let Some(g) = &self.scenario.d1 { self.editor.rule_1d = Some(Rule1DEdit::from_rule(&g.rule)); }
if let Some(edit) = &mut self.editor.rule_1d {
// Subrules list (scrollable)
let mut remove_idx: Option<usize> = None;
let mut move_up_idx: Option<usize> = None;
let mut move_down_idx: Option<usize> = None;
ui.set_min_height(240.0);
egui::ScrollArea::vertical().show(ui, |ui| {
for i in 0..edit.subrules.len() {
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label(format!("Subrule #{}", i+1));
if ui.button("Remove").clicked() { remove_idx = Some(i); }
});
let ty_names = &names;
let sub = &mut edit.subrules[i];
// current
type_combo(
ui,
("d1_cur", i),
"current:",
"Center cell must currently be this state for the subrule to apply.",
ty_names,
&mut sub.current,
);
// criteria
type_combo(
ui,
("d1_crit", i),
"criteria:",
"Neighbor cells equal to this state are treated as 1s in the Wolfram pattern; others are 0s.",
ty_names,
&mut sub.criteria,
);
// output
type_combo(
ui,
("d1_out", i),
"output:",
"The new state to set when this subrule matches.",
ty_names,
&mut sub.output,
);
// code and n
ui.horizontal(|ui| {
ui.label("wolfram code:")
.on_hover_text("Bitmask for patterns over a (2n+1) window of neighbors: 1 = match triggers. Indexing uses a binary window where neighbors equal to 'criteria' are 1.");
ui.text_edit_singleline(&mut sub.wolfram_code)
.on_hover_text("Enter a non-negative integer (u128). For n=1 there are 2^(3)=8 patterns; for larger n the number grows quickly.");
ui.label("n:")
.on_hover_text("Neighborhood radius (>=1). The window size is 2n+1 around the center cell.");
ui.add(egui::DragValue::new(&mut sub.n).range(1..=8))
.on_hover_text("Radius n between 1 and 8.");
});
ui.horizontal(|ui| {
if ui.button("Up").clicked() { move_up_idx = Some(i); }
if ui.button("Down").clicked() { move_down_idx = Some(i); }
});
// randomness
randomness_control(
ui,
&mut sub.randomness_enabled,
&mut sub.randomness_value,
"Optional stochasticity: when enabled, the match will only apply with probability 1 - p.",
);
});
}
});
if let Some(i) = move_up_idx && i > 0 { edit.subrules.swap(i, i - 1); }
if let Some(i) = move_down_idx && i + 1 < edit.subrules.len() { edit.subrules.swap(i, i + 1); }
if let Some(idx) = remove_idx { edit.subrules.remove(idx); }
if ui.button("Add subrule").clicked() {
edit.subrules.push(Rule1DSubruleEdit{ current: INACTIVE.to_string(), criteria: INACTIVE.to_string(), wolfram_code: "0".into(), n: 1, randomness_enabled: false, randomness_value: 0.0, output: INACTIVE.to_string()});
}
if ui.button("Apply to grid").clicked() {
pending.push(Action::ApplyRule);
}
} else {
ui.label("No 1D grid loaded.");
}
}
Dim::D2 => {
if self.editor.rule_2d.is_none()
&& let Some(g) = &self.scenario.d2 { self.editor.rule_2d = Some(Rule2DEdit::from_rule(&g.rule)); }
if let Some(edit) = &mut self.editor.rule_2d {
let mut remove_idx: Option<usize> = None;
let mut move_up_idx: Option<usize> = None;
let mut move_down_idx: Option<usize> = None;
ui.set_min_height(240.0);
egui::ScrollArea::vertical().show(ui, |ui| {
for i in 0..edit.subrules.len() {
ui.group(|ui| {
ui.horizontal(|ui| {
ui.label(format!("Subrule #{}", i+1));
if ui.button("Remove").clicked() { remove_idx = Some(i); }
});
let ty_names = &names;
let sub = &mut edit.subrules[i];
// current
type_combo(
ui,
("d2_cur", i),
"current:",
"Center cell must currently be this state for the subrule to apply.",
ty_names,
&mut sub.current,
);
// criteria
type_combo(
ui,
("d2_crit", i),
"criteria:",
"Neighbor cells of this state are counted within the chosen neighborhood.",
ty_names,
&mut sub.criteria,
);
// output
type_combo(
ui,
("d2_out", i),
"output:",
"The new state to set when this subrule matches.",
ty_names,
&mut sub.output,
);
// neighborhood modifiers
ui.horizontal(|ui| {
ui.label("count:")
.on_hover_text("Baseline neighbor count for comparison. See 'op' for how it is used.");
ui.add(egui::DragValue::new(&mut sub.count).range(0..=99))
.on_hover_text("Set the baseline count between 0 and 99.");
ui.label("op:")
.on_hover_text("Comparison: gt means >= count, lt means <= count, eq means exactly count. With a limit, you can specify a range.");
let mut op = sub.op;
egui::ComboBox::from_id_salt(format!("d2_op_{}", i))
.selected_text(match op { CountOp::Lt=>"lt", CountOp::Gt=>"gt", CountOp::Eq=>"eq" })
.show_ui(ui, |ui| {
ui.selectable_value(&mut op, CountOp::Lt, "lt");
ui.selectable_value(&mut op, CountOp::Gt, "gt");
ui.selectable_value(&mut op, CountOp::Eq, "eq");
});
sub.op = op;
});
ui.horizontal(|ui| {
ui.checkbox(&mut sub.limit_enabled, "limit")
.on_hover_text("Optional second bound to create a range: with op=gt, checks count in [count..=limit]; with op=lt, checks count in [limit..=count].");
if sub.limit_enabled {
ui.add(egui::DragValue::new(&mut sub.limit_value).range(0..=99))
.on_hover_text("Inclusive bound for the range comparison.");
}
});
ui.horizontal(|ui| {
ui.label("range n:")
.on_hover_text("Neighborhood range (>=1). The square window is (2n+1)^2, filtered by the chosen neighborhood type.");
ui.add(egui::DragValue::new(&mut sub.range).range(1..=8))
.on_hover_text("Set range n between 1 and 8.");
ui.label("neighborhood:")
.on_hover_ui(|ui| {
ui.label("Neighborhood shape around the center (@):");
let diag = CellaApp::neighborhood_ascii(sub.range, sub.neighborhood);
ui.monospace(diag);
ui.small("Legend: @ center, # counted neighbor, . outside");
ui.separator();
ui.label("Moore = square; VonNeumann = Manhattan distance; Langton = diagonals; StraightLine = cardinal lines only; Knight = chess knight L-moves (range = max hops)");
});
let mut nb = sub.neighborhood;
egui::ComboBox::from_id_salt(format!("d2_nh_{}", i))
.selected_text(match nb { Neighborhood2D::Moore=>"Moore", Neighborhood2D::VonNeumann=>"VonNeumann", Neighborhood2D::Langton =>"Langton", Neighborhood2D::StraightLine=>"StraightLine", Neighborhood2D::Knight=>"Knight" })
.show_ui(ui, |ui| {
ui.selectable_value(&mut nb, Neighborhood2D::Moore, "Moore");
ui.selectable_value(&mut nb, Neighborhood2D::VonNeumann, "VonNeumann");
ui.selectable_value(&mut nb, Neighborhood2D::Langton, "Langton");
ui.selectable_value(&mut nb, Neighborhood2D::StraightLine, "StraightLine");
ui.selectable_value(&mut nb, Neighborhood2D::Knight, "Knight");
});
sub.neighborhood = nb;
});
randomness_control(
ui,
&mut sub.randomness_enabled,
&mut sub.randomness_value,
"Optional stochasticity: when enabled, the match will only apply with probability 1 - p.",
);
ui.horizontal(|ui| {
if ui.button("Up").clicked() { move_up_idx = Some(i); }
if ui.button("Down").clicked() { move_down_idx = Some(i); }
});
});
}
});
if let Some(i) = move_up_idx && i > 0 { edit.subrules.swap(i, i - 1); }
if let Some(i) = move_down_idx && i + 1 < edit.subrules.len() { edit.subrules.swap(i, i + 1); }
if let Some(idx) = remove_idx { edit.subrules.remove(idx); }
if ui.button("Add subrule").clicked() {
edit.subrules.push(Rule2DSubruleEdit{ current: INACTIVE.to_string(), criteria: INACTIVE.to_string(), count: 0, op: CountOp::Gt, limit_enabled: false, limit_value: 0, range: 1, neighborhood: Neighborhood2D::Moore, randomness_enabled: false, randomness_value: 0.0, output: INACTIVE.to_string() });
}
if ui.button("Apply to grid").clicked() {
pending.push(Action::ApplyRule);
}
} else {
ui.label("No 2D grid loaded.");
}
}
}
} else {
ui.label("No grid loaded.");
}
if let Some(err) = &self.editor.error_msg { ui.colored_label(egui::Color32::RED, format!("Rule error: {}", err)); }
}
for a in pending {
self.push(a);
}
}
/// Build the color editor panel, including the Inactive color.
pub(in crate::gui) fn refresh_rule_editor_from_current(&mut self) {
self.editor.error_msg = None;
match self.scenario.dim {
Some(Dim::D1) => {
if let Some(g) = &self.scenario.d1 {
self.editor.rule_1d = Some(Rule1DEdit::from_rule(&g.rule));
self.editor.rule_2d = None;
}
}
Some(Dim::D2) => {
if let Some(g) = &self.scenario.d2 {
self.editor.rule_2d = Some(Rule2DEdit::from_rule(&g.rule));
self.editor.rule_1d = None;
}
}
None => {
self.editor.rule_1d = None;
self.editor.rule_2d = None;
}
}
}
/// Show a collapsible panel with per-type statistics (current and peak counts),
/// and a running history chart (fixed-size window) similar to Task Manager.
pub(in crate::gui) fn neighborhood_ascii(range: u8, kind: Neighborhood2D) -> String {
let n = range as i32;
let mut out = String::new();
let name = match kind {
Neighborhood2D::Moore => "Moore",
Neighborhood2D::VonNeumann => "VonNeumann",
Neighborhood2D::Langton => "Langton",
Neighborhood2D::StraightLine => "StraightLine",
Neighborhood2D::Knight => "Knight",
};
out.push_str(&format!("{} (n={})\n", name, range));
// Knight moves can reach up to 2*range steps per axis, so widen the window.
let half = if kind == Neighborhood2D::Knight {
n * 2
} else {
n
};
for dy in -half..=half {
for dx in -half..=half {
if dx == 0 && dy == 0 {
out.push('@');
} else {
let inside = neighborhood_contains(dx, dy, n, kind);
out.push(if inside { '#' } else { '.' });
}
}
if dy != half {
out.push('\n');
}
}
out
}
}