|
| 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