Skip to content

Commit 66ea131

Browse files
committed
Adds egui example
Adds an example demonstrating the integration of the asciiquarium-rust library with egui. The example showcases how to create a simple aquarium simulation within an egui application, allowing users to interact with the aquarium by adjusting the grid size, frame rate, and theme colors.
1 parent 2a861d8 commit 66ea131

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ path = "src/lib.rs"
1212

1313
[dependencies]
1414
egui = "0.27"
15+
16+
[dev-dependencies]
17+
eframe = "0.27"
18+
rand = "0.8"

examples/egui_demo.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use std::time::Duration;
2+
3+
use asciiquarium_rust::{
4+
get_all_fish_assets, update_aquarium, AquariumState, AsciiquariumTheme, AsciiquariumWidget,
5+
FishInstance,
6+
};
7+
use eframe::egui;
8+
use rand::Rng;
9+
10+
fn main() -> eframe::Result<()> {
11+
let native_options = eframe::NativeOptions::default();
12+
eframe::run_native(
13+
"Asciiquarium egui demo",
14+
native_options,
15+
Box::new(|_cc| Box::new(MyApp::new())),
16+
)
17+
}
18+
19+
struct MyApp {
20+
assets: Vec<asciiquarium_rust::FishArt>,
21+
state: AquariumState,
22+
theme: AsciiquariumTheme,
23+
frame_ms: u64,
24+
bg_enabled: bool,
25+
}
26+
27+
impl MyApp {
28+
fn new() -> Self {
29+
let assets = get_all_fish_assets();
30+
31+
// Choose an initial grid size. You can make this dynamic later if desired.
32+
let size = (80usize, 24usize);
33+
34+
let mut state = AquariumState {
35+
size,
36+
fishes: Vec::new(),
37+
};
38+
39+
// Seed with a few random fish
40+
for _ in 0..6 {
41+
spawn_random_fish(&mut state, assets.len());
42+
}
43+
44+
let theme = AsciiquariumTheme {
45+
text_color: egui::Color32::from_rgb(180, 220, 255),
46+
background: Some(egui::Color32::from_rgb(8, 12, 16)),
47+
wrap: false,
48+
};
49+
50+
Self {
51+
assets,
52+
state,
53+
theme,
54+
frame_ms: 33,
55+
bg_enabled: true,
56+
}
57+
}
58+
}
59+
60+
impl eframe::App for MyApp {
61+
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
62+
// Drive animation based on a simple frame duration.
63+
update_aquarium(&mut self.state, &self.assets);
64+
ctx.request_repaint_after(Duration::from_millis(self.frame_ms));
65+
66+
egui::TopBottomPanel::top("top_controls").show(ctx, |ui| {
67+
ui.horizontal(|ui| {
68+
ui.label("Grid:");
69+
// Keep grid integers reasonable. Avoid sliders for usize to reduce friction.
70+
if ui.button("-W").clicked() && self.state.size.0 > 10 {
71+
self.state.size.0 -= 2;
72+
}
73+
if ui.button("+W").clicked() {
74+
self.state.size.0 += 2;
75+
}
76+
if ui.button("-H").clicked() && self.state.size.1 > 5 {
77+
self.state.size.1 -= 1;
78+
}
79+
if ui.button("+H").clicked() {
80+
self.state.size.1 += 1;
81+
}
82+
83+
ui.separator();
84+
85+
ui.label("Frame (ms):");
86+
if ui.button("-").clicked() && self.frame_ms > 5 {
87+
self.frame_ms -= 2;
88+
}
89+
if ui.button("+").clicked() && self.frame_ms < 1000 {
90+
self.frame_ms += 2;
91+
}
92+
93+
ui.separator();
94+
95+
ui.label("Theme:");
96+
ui.color_edit_button_srgba(&mut self.theme.text_color);
97+
ui.checkbox(&mut self.bg_enabled, "Background");
98+
if self.bg_enabled {
99+
// Ensure background stays Some when enabled
100+
if self.theme.background.is_none() {
101+
self.theme.background = Some(egui::Color32::from_rgb(8, 12, 16));
102+
}
103+
if let Some(bg) = &mut self.theme.background {
104+
ui.color_edit_button_srgba(bg);
105+
}
106+
} else {
107+
self.theme.background = None;
108+
}
109+
110+
ui.separator();
111+
112+
if ui.button("Add fish").clicked() {
113+
spawn_random_fish(&mut self.state, self.assets.len());
114+
}
115+
if ui.button("Reset").clicked() {
116+
self.state.fishes.clear();
117+
for _ in 0..6 {
118+
spawn_random_fish(&mut self.state, self.assets.len());
119+
}
120+
}
121+
});
122+
});
123+
124+
egui::CentralPanel::default().show(ctx, |ui| {
125+
// Render widget as a single monospace label
126+
ui.add(AsciiquariumWidget {
127+
state: &self.state,
128+
assets: &self.assets,
129+
theme: &self.theme,
130+
});
131+
});
132+
}
133+
}
134+
135+
fn spawn_random_fish(state: &mut AquariumState, asset_count: usize) {
136+
if asset_count == 0 {
137+
return;
138+
}
139+
let mut rng = rand::thread_rng();
140+
141+
let idx = rng.gen_range(0..asset_count);
142+
143+
// Random position within grid; update() will clamp on edges using asset size
144+
let max_x = if state.size.0 > 0 {
145+
state.size.0 - 1
146+
} else {
147+
0
148+
};
149+
let max_y = if state.size.1 > 0 {
150+
state.size.1 - 1
151+
} else {
152+
0
153+
};
154+
let x = rng.gen_range(0..=max_x) as f32;
155+
let y = rng.gen_range(0..=max_y) as f32;
156+
157+
// Random velocity with minimum magnitude to avoid stationary fish
158+
let mut vx = rng.gen_range(-0.5_f32..=0.5_f32);
159+
let mut vy = rng.gen_range(-0.25_f32..=0.25_f32);
160+
if vx.abs() < 0.05 {
161+
vx = if vx.is_sign_negative() { -0.08 } else { 0.08 };
162+
}
163+
if vy.abs() < 0.02 {
164+
vy = if vy.is_sign_negative() { -0.03 } else { 0.03 };
165+
}
166+
167+
state.fishes.push(FishInstance {
168+
fish_art_index: idx,
169+
position: (x, y),
170+
velocity: (vx, vy),
171+
});
172+
}

0 commit comments

Comments
 (0)