Render a fully functional Winamp-style main window using the loaded WSZ skin data, with pixel-perfect appearance and interactive controls.
oneamp-desktop/src/
├── wsz_ui/
│ ├── mod.rs # Public exports
│ ├── renderer.rs # Core WSZ rendering engine
│ ├── main_window.rs # Main window (275×116)
│ ├── components/
│ │ ├── mod.rs
│ │ ├── buttons.rs # Play, Pause, Stop, etc.
│ │ ├── sliders.rs # Position, Volume, Balance
│ │ ├── display.rs # Time, Title, Info
│ │ └── vuemeter.rs # VU-meter visualization
│ └── texture_cache.rs # egui texture management
Converts BitmapAtlas to egui textures with pixel-perfect rendering.
pub struct WszRenderer {
texture_cache: HashMap<String, TextureHandle>,
skin: Arc<WszSkin>,
}
impl WszRenderer {
pub fn new(ctx: &egui::Context, skin: WszSkin) -> Self;
pub fn get_texture(&mut self,
ctx: &egui::Context,
component: &SkinComponent) -> Option<&TextureHandle>;
pub fn render_component(&mut self,
ui: &mut egui::Ui,
component: &SkinComponent,
pos: egui::Pos2);
pub fn render_region(&mut self,
ui: &mut egui::Ui,
component: &SkinComponent,
region: BitmapRegion,
pos: egui::Pos2);
}pub struct WszMainWindow {
renderer: WszRenderer,
// State
is_playing: bool,
is_paused: bool,
current_time: f32,
total_time: f32,
volume: f32,
balance: f32,
// UI state
dragging_position: bool,
dragging_volume: bool,
button_states: HashMap<Button, ButtonState>,
}
impl WszMainWindow {
pub fn new(ctx: &egui::Context, skin: WszSkin) -> Self;
pub fn show(&mut self, ctx: &egui::Context) {
egui::Window::new("OneAmp")
.fixed_size([275.0 * SCALE, 116.0 * SCALE])
.frame(egui::Frame::none())
.show(ctx, |ui| {
self.render_background(ui);
self.render_controls(ui);
self.render_displays(ui);
self.render_sliders(ui);
self.handle_input(ui);
});
}
fn render_background(&mut self, ui: &mut egui::Ui);
fn render_controls(&mut self, ui: &mut egui::Ui);
fn render_displays(&mut self, ui: &mut egui::Ui);
fn render_sliders(&mut self, ui: &mut egui::Ui);
fn handle_input(&mut self, ui: &mut egui::Ui);
}Standard Winamp buttons with pixel-perfect positions.
pub enum WinampButton {
Previous,
Play,
Pause,
Stop,
Next,
Eject,
}
pub struct ButtonComponent {
button_type: WinampButton,
position: (u32, u32), // Position in main window
size: (u32, u32), // Button size
sprite_coords: (u32, u32), // Coords in cbuttons.bmp
state: ButtonState,
}
impl ButtonComponent {
pub fn render(&self,
ui: &mut egui::Ui,
renderer: &mut WszRenderer) -> bool;
pub fn is_hovered(&self, mouse_pos: egui::Pos2) -> bool;
pub fn update_state(&mut self,
is_hovered: bool,
is_pressed: bool);
}Standard Button Positions (Winamp Classic):
const BUTTON_PREVIOUS: (u32, u32) = (16, 88); // 23×18 px
const BUTTON_PLAY: (u32, u32) = (39, 88); // 23×18 px
const BUTTON_PAUSE: (u32, u32) = (62, 88); // 23×18 px
const BUTTON_STOP: (u32, u32) = (85, 88); // 23×18 px
const BUTTON_NEXT: (u32, u32) = (108, 88); // 22×18 px
const BUTTON_EJECT: (u32, u32) = (136, 89); // 22×16 pxDraggable position bar with 29 frames animation.
pub struct PositionSlider {
position: (u32, u32), // (16, 72) in main window
width: u32, // 248 pixels
current_frame: u32, // 0-28
is_dragging: bool,
progress: f32, // 0.0 - 1.0
}
impl PositionSlider {
pub fn render(&self,
ui: &mut egui::Ui,
renderer: &mut WszRenderer);
pub fn handle_input(&mut self,
ui: &egui::Ui,
mouse_pos: egui::Pos2) -> Option<f32>;
fn get_frame_for_position(&self, x: u32) -> u32 {
((x - self.position.0) * 29 / self.width).min(28)
}
}Vertical sliders with 28 positions each.
pub struct VolumeSlider {
position: (u32, u32), // (107, 57) for volume
value: f32, // 0.0 - 1.0
frame: u32, // 0-27
}
pub struct BalanceSlider {
position: (u32, u32), // (177, 57) for balance
value: f32, // -1.0 to 1.0
frame: u32, // 0-27 (14 = center)
}Time display using numbers.bmp bitmap font.
pub struct DigitalDisplay {
position: (u32, u32), // (48, 26) for time
current_time: f32,
show_remaining: bool, // Toggle between elapsed/remaining
}
impl DigitalDisplay {
pub fn render(&self,
ui: &mut egui::Ui,
renderer: &mut WszRenderer) {
let time_str = self.format_time();
for (i, ch) in time_str.chars().enumerate() {
let digit_sprite = self.get_digit_sprite(ch);
let x = self.position.0 + i as u32 * 9;
renderer.render_region(ui, &SkinComponent::Numbers,
digit_sprite,
egui::pos2(x as f32, self.position.1 as f32));
}
}
fn format_time(&self) -> String {
let time = if self.show_remaining {
-(self.total_time - self.current_time)
} else {
self.current_time
};
let mins = (time.abs() as i32) / 60;
let secs = (time.abs() as i32) % 60;
if time < 0.0 {
format!("-{:01}:{:02}", mins, secs)
} else {
format!(" {:01}:{:02}", mins, secs)
}
}
fn get_digit_sprite(&self, ch: char) -> BitmapRegion {
let index = match ch {
'0'..='9' => ch as u32 - '0' as u32,
'-' => 10,
' ' => 11, // Blank
':' => 10, // Use - for colon in some skins
_ => 11,
};
// Each digit is 9×13 pixels in numbers.bmp
// Extract the appropriate region
}
}Real-time audio level visualization.
pub struct VuMeter {
position: (u32, u32), // (24, 43) for mono/stereo
left_level: f32, // 0.0 - 1.0
right_level: f32, // 0.0 - 1.0
}
impl VuMeter {
pub fn update_levels(&mut self, audio_samples: &[f32]);
pub fn render(&self,
ui: &mut egui::Ui,
renderer: &mut WszRenderer) {
// Render appropriate frame from monoster.bmp
// based on current levels (28 frames per channel)
let left_frame = (self.left_level * 27.0) as u32;
let right_frame = (self.right_level * 27.0) as u32;
// Extract and render frames
}
}Support integer scaling for crisp pixels:
const SCALE_OPTIONS: &[f32] = &[1.0, 2.0, 3.0, 4.0];
impl WszMainWindow {
fn get_window_size(&self, scale: f32) -> [f32; 2] {
[275.0 * scale, 116.0 * scale]
}
}Disable texture filtering for pixel-perfect rendering:
use egui::TextureOptions;
let texture_options = TextureOptions {
magnification: egui::TextureFilter::Nearest,
minification: egui::TextureFilter::Nearest,
};Use exact pixel coordinates:
// Convert skin coordinates to egui coordinates
fn skin_to_screen(skin_x: u32, skin_y: u32, scale: f32) -> egui::Pos2 {
egui::pos2(skin_x as f32 * scale, skin_y as f32 * scale)
}
// Convert screen coordinates back to skin coordinates
fn screen_to_skin(pos: egui::Pos2, scale: f32) -> (u32, u32) {
((pos.x / scale) as u32, (pos.y / scale) as u32)
}impl WszMainWindow {
pub fn update(&mut self, events: &[AudioEvent]) {
for event in events {
match event {
AudioEvent::Playing => {
self.is_playing = true;
self.is_paused = false;
}
AudioEvent::Paused => {
self.is_paused = true;
}
AudioEvent::Position(current, total) => {
self.current_time = *current;
self.total_time = *total;
}
AudioEvent::VolumeChanged(vol, _) => {
self.volume = *vol;
}
AudioEvent::VisualizationData(samples) => {
self.vuemeter.update_levels(samples);
}
_ => {}
}
}
}
pub fn handle_button_click(&mut self,
button: WinampButton,
audio_engine: &AudioEngine) {
match button {
WinampButton::Play => {
audio_engine.send_command(AudioCommand::Resume);
}
WinampButton::Pause => {
audio_engine.send_command(AudioCommand::Pause);
}
WinampButton::Stop => {
audio_engine.send_command(AudioCommand::Stop);
}
WinampButton::Next => {
audio_engine.send_command(AudioCommand::Next);
}
WinampButton::Previous => {
audio_engine.send_command(AudioCommand::Previous);
}
_ => {}
}
}
}Create test skins with known layouts:
#[test]
fn test_button_positions() {
let skin = load_test_skin();
let window = WszMainWindow::new(&ctx, skin);
// Verify button positions match Winamp spec
assert_eq!(window.buttons[Button::Play].position, (39, 88));
}#[test]
fn test_position_slider_drag() {
let mut slider = PositionSlider::new();
// Simulate drag from 0% to 50%
let progress = slider.handle_input(&ui, egui::pos2(140.0, 72.0));
assert!(progress.is_some());
assert!((progress.unwrap() - 0.5).abs() < 0.05);
}- Frame rate: 60 FPS constant
- Input latency: < 16ms
- Texture memory: < 10 MB for typical skin
- CPU usage: < 5% idle, < 10% during playback
// In main.rs
use oneamp_core::wsz::WszLoader;
use oneamp_desktop::wsz_ui::WszMainWindow;
fn main() {
let skin = WszLoader::load_from_file("skins/classic.wsz")
.expect("Failed to load skin");
let app = OneAmpApp {
wsz_window: Some(WszMainWindow::new(&ctx, skin)),
audio_engine: AudioEngine::new().unwrap(),
// ...
};
eframe::run_native(/* ... */);
}
impl eframe::App for OneAmpApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Process audio events
while let Some(event) = self.audio_engine.try_recv_event() {
if let Some(wsz) = &mut self.wsz_window {
wsz.update(&[event]);
}
}
// Render WSZ window
if let Some(wsz) = &mut self.wsz_window {
wsz.show(ctx);
}
}
}┌───────────────────────────────────────────────────────────┐
│ [Winamp v2.91] [O][o][_][x] │ ← titlebar.bmp (0, 0)
├───────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌──────────┐ ╔══════╗ ╔═══╗ │
│ │ Oscilloscope│ │ -0:42 │ ║ 160 ║ ║ ♪ ║ │ ← numbers.bmp (48, 26)
│ │ ~~~ │ │ │ ║ kbps ║ ║ ║ │
│ └────────────┘ └──────────┘ ╚══════╝ ╚═══╝ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [███████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░] │ │ ← posbar.bmp (16, 72)
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [◀◀] [▶] [⏸] [■] [▶▶] [⏏] │ ← cbuttons.bmp (16, 88)
│ │
│ [EQ] [PL] VOL ▓▓▓ BAL ▓▓▓ [●] [≡] [□] [×] │
│ ░░░ ░░░ │ ← volume.bmp, balance.bmp
│ │
└───────────────────────────────────────────────────────────┘
0,0 275,116
Once Phase 2 is complete, we'll have a fully functional Winamp-style main window. Phase 3 will add:
- Equalizer window (EQ)
- Playlist editor
- Shade mode (mini player)
- Double-size mode
- Custom window shapes from region.txt
Ready to start Phase 2? The infrastructure from Phase 1 provides everything needed to begin rendering WSZ skins in the OneAmp desktop application.