Skip to content

Commit 1d804ae

Browse files
committed
Implements Asciiquarium widget for egui
Adds a themeable, stateless Asciiquarium widget for `egui`. The widget renders a classic ASCII aquarium using a single `egui::Label`. The animation state is owned and updated by the parent application. Includes initial fish assets and example usage. - Adds core data structures for fish, aquarium state, and theme. - Implements update logic with wall-bounce physics. - Implements rendering from state to string using a character grid and clipping. - Provides a widget for `egui` that renders the aquarium. - Includes CI workflow for formatting, linting, building, and testing. - Adds `.gitignore` file to ignore build artifacts and editor files.
1 parent 8bdb7d4 commit 1d804ae

File tree

14 files changed

+909
-0
lines changed

14 files changed

+909
-0
lines changed

.github/workflows/ci.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
concurrency:
10+
group: ci-${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
env:
17+
CARGO_TERM_COLOR: always
18+
19+
jobs:
20+
fmt:
21+
name: rustfmt
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Checkout sources
25+
uses: actions/checkout@v4
26+
- name: Install Rust (stable) + rustfmt
27+
uses: dtolnay/rust-toolchain@stable
28+
with:
29+
components: rustfmt
30+
- name: Cache cargo
31+
uses: Swatinem/rust-cache@v2
32+
- name: Check formatting
33+
run: cargo fmt --all -- --check
34+
35+
clippy:
36+
name: clippy
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Checkout sources
40+
uses: actions/checkout@v4
41+
- name: Install Rust (stable) + clippy
42+
uses: dtolnay/rust-toolchain@stable
43+
with:
44+
components: clippy
45+
- name: Cache cargo
46+
uses: Swatinem/rust-cache@v2
47+
- name: Lint with clippy (deny warnings)
48+
run: cargo clippy --all-targets --all-features -- -D warnings
49+
50+
build:
51+
name: build (${{ matrix.os }})
52+
runs-on: ${{ matrix.os }}
53+
strategy:
54+
fail-fast: false
55+
matrix:
56+
os: [ubuntu-latest, macos-latest, windows-latest]
57+
steps:
58+
- name: Checkout sources
59+
uses: actions/checkout@v4
60+
- name: Install Rust (stable)
61+
uses: dtolnay/rust-toolchain@stable
62+
- name: Cache cargo
63+
uses: Swatinem/rust-cache@v2
64+
- name: Build
65+
run: cargo build --all-targets --all-features
66+
67+
test:
68+
name: test
69+
runs-on: ubuntu-latest
70+
steps:
71+
- name: Checkout sources
72+
uses: actions/checkout@v4
73+
- name: Install Rust (stable)
74+
uses: dtolnay/rust-toolchain@stable
75+
- name: Cache cargo
76+
uses: Swatinem/rust-cache@v2
77+
- name: Run tests
78+
run: cargo test --all-features --no-fail-fast

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Build artifacts
2+
/target/
3+
4+
# For libraries, do not commit Cargo.lock
5+
/Cargo.lock
6+
7+
# Editors/OS
8+
/.idea/
9+
/.vscode/
10+
.DS_Store
11+
12+
# Logs
13+
*.log
14+
15+
# Dev internal docs (not for distribution)
16+
/plan.md
17+
/rust_rules.md

Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "asciiquarium-rust"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Asciiquarium widget and utilities for egui"
6+
# license = "" # Set an appropriate license for your project if desired
7+
# repository = ""
8+
9+
[lib]
10+
name = "asciiquarium_rust"
11+
path = "src/lib.rs"
12+
13+
[dependencies]
14+
egui = "0.27"

README.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Asciiquarium (Rust + egui)
2+
3+
A stateless, themeable Asciiquarium widget for `egui`. The widget renders a classic ASCII aquarium using a single `egui::Label`, while your parent application owns and updates the animation state.
4+
5+
- Stateless: The widget only renders from state and assets.
6+
- Themeable: No hardcoded styles. Colors and wrapping are derived from a theme you pass in.
7+
- Simple physics: Fish move with velocity and bounce off the aquarium edges.
8+
- No panics, no unwrap/expect, no unnecessary clones.
9+
10+
Archived originals from the Perl-based Asciiquarium are kept under `archive/original/` for reference.
11+
12+
## Quickstart
13+
14+
1) Add the dependency in your `Cargo.toml`:
15+
16+
[dependencies]
17+
egui = "0.27"
18+
asciiquarium_rust = { path = "." } # or from your git repo/registry
19+
20+
2) Prepare assets and state in your app:
21+
22+
use asciiquarium_rust::{
23+
get_fish_assets, update_aquarium, AsciiquariumTheme, AsciiquariumWidget,
24+
AquariumState, FishInstance,
25+
};
26+
27+
// Build assets once (e.g., at startup).
28+
let assets = get_fish_assets();
29+
30+
// Create your aquarium state (owned by the parent app).
31+
let mut state = AquariumState {
32+
size: (80, 24), // character grid width x height
33+
fishes: vec![
34+
FishInstance {
35+
fish_art_index: 0,
36+
position: (2.0, 3.0),
37+
velocity: (0.4, 0.0),
38+
},
39+
FishInstance {
40+
fish_art_index: 1,
41+
position: (30.0, 10.0),
42+
velocity: (-0.3, 0.1),
43+
},
44+
],
45+
};
46+
47+
3) In your app’s update loop, update and render:
48+
49+
// Update the simulation (e.g., once per frame or on your own tick).
50+
update_aquarium(&mut state, &assets);
51+
52+
// Derive styles from your theme (no hardcoded styles).
53+
let theme = AsciiquariumTheme {
54+
text_color: egui::Color32::from_rgb(180, 220, 255),
55+
background: Some(egui::Color32::from_rgb(8, 12, 16)),
56+
wrap: false, // keep ASCII grid alignment
57+
};
58+
59+
// Render: a single monospace label with your aquarium.
60+
ui.add(AsciiquariumWidget {
61+
state: &state,
62+
assets: &assets,
63+
theme: &theme,
64+
});
65+
66+
## Theming
67+
68+
Follow the “Design for Theming” rule: the component does not hardcode colors or styles. Everything flows through `AsciiquariumTheme`.
69+
70+
- `text_color`: The color used for the ASCII characters.
71+
- `background`: Optional background fill for the label area.
72+
- `wrap`: Line wrapping for the label (usually `false` to preserve ASCII alignment).
73+
74+
Example themes:
75+
76+
// Light theme
77+
let light = AsciiquariumTheme {
78+
text_color: egui::Color32::from_rgb(40, 40, 40),
79+
background: Some(egui::Color32::from_rgb(245, 245, 245)),
80+
wrap: false,
81+
};
82+
83+
// High contrast
84+
let high_contrast = AsciiquariumTheme {
85+
text_color: egui::Color32::WHITE,
86+
background: Some(egui::Color32::BLACK),
87+
wrap: false,
88+
};
89+
90+
## API Overview
91+
92+
- `FishArt`:
93+
- `art: &'static str`
94+
- `width: usize`
95+
- `height: usize`
96+
97+
- `FishInstance`:
98+
- `fish_art_index: usize`
99+
- `position: (f32, f32)` // top-left in character coordinates
100+
- `velocity: (f32, f32)` // characters per tick
101+
102+
- `AquariumState`:
103+
- `size: (usize, usize)` // width x height in characters
104+
- `fishes: Vec<FishInstance>`
105+
106+
- Functions:
107+
- `get_fish_assets() -> Vec<FishArt>`
108+
- `update_aquarium(state: &mut AquariumState, assets: &[FishArt])`
109+
- `render_aquarium_to_string(state: &AquariumState, assets: &[FishArt]) -> String`
110+
111+
- Widget:
112+
- `AsciiquariumWidget<'a> { state: &'a AquariumState, assets: &'a [FishArt], theme: &'a AsciiquariumTheme }`
113+
- Implements `egui::Widget` and renders a single, monospace label.
114+
115+
## Design Notes
116+
117+
- Stateless rendering: The widget takes immutable `&AquariumState` and `&[FishArt]` and renders a single string. No side effects, no mutation.
118+
- Parent-managed animation: The parent application updates `AquariumState` each tick using `update_aquarium`.
119+
- Float-to-int: Rendering uses `floor()` for stable projection and less jitter.
120+
- Bounds and clipping: Rendering clips safely; later fish in the slice overdraw earlier ones.
121+
- Dimensions: `AquariumState.size` is in character cells. Choose a fixed grid (e.g., 80x24) or set it based on your layout needs.
122+
123+
## Testing
124+
125+
Run unit tests:
126+
127+
cargo test
128+
129+
Tests cover:
130+
- Edge bounce behavior
131+
- Left-edge clipping in rendering
132+
- Asset measurement correctness
133+
134+
## Tips & Troubleshooting
135+
136+
- Misaligned ASCII: Ensure `theme.wrap` is `false` and that the container does not force wrapping. The widget uses `RichText::monospace()`.
137+
- Too small or clipped label: The rendered string’s dimensions are exactly `size.1` lines by `size.0` columns. Place it in a container large enough to display without wrapping or scaling.
138+
- Frame timing: If motion is too fast or slow, adjust fish velocities or call `update_aquarium` at your preferred tick rate.
139+
140+
## Roadmap
141+
142+
- Additional fish and sea creatures from the classic Asciiquarium
143+
- Configurable z-ordering and layering
144+
- Optional wrap-around movement
145+
- Simple scene randomizer utilities (spawn fish with random velocity and positions)
146+
147+
## Contributing
148+
149+
- Follow the Rust rules in `rust_rules.md`:
150+
- No `unwrap`/`expect` in application logic
151+
- No panics; handle errors gracefully
152+
- No unnecessary clones; prefer references
153+
- Keep modules small and focused; single responsibility
154+
- Rendering is a pure function of state
155+
- Design for theming (no hardcoded styles)
156+
- Use `rustfmt` and `clippy` with zero warnings.
157+
158+
## License and Credits
159+
160+
- The original Perl Asciiquarium (Kirk Baucom) materials are archived under `archive/original/`.
161+
- This crate provides an `egui`-based Rust implementation with a stateless, themeable widget design.
162+
- ASCII art in this crate is a minimal starter set for demonstration. Expand or replace as needed per your project’s licensing requirements.

0 commit comments

Comments
 (0)