Skip to content

Latest commit

 

History

History
492 lines (395 loc) · 14.5 KB

File metadata and controls

492 lines (395 loc) · 14.5 KB

Phase 2 Preview: Main Window Rendering

Goal

Render a fully functional Winamp-style main window using the loaded WSZ skin data, with pixel-perfect appearance and interactive controls.

Architecture Overview

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

Key Components to Implement

1. WSZ Renderer (renderer.rs)

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);
}

2. Main Window (main_window.rs)

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);
}

3. Control Buttons (components/buttons.rs)

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 px

4. Position Slider (components/sliders.rs)

Draggable 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)
    }
}

5. Volume/Balance Sliders

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

6. Digital Display (components/display.rs)

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

7. VU-Meter (components/vuemeter.rs)

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

Pixel-Perfect Rendering Strategy

Scaling

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

Texture Filtering

Disable texture filtering for pixel-perfect rendering:

use egui::TextureOptions;

let texture_options = TextureOptions {
    magnification: egui::TextureFilter::Nearest,
    minification: egui::TextureFilter::Nearest,
};

Coordinate System

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

Integration with Existing OneAmp

Audio Engine Communication

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);
            }
            _ => {}
        }
    }
}

Testing Strategy

Visual Tests

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));
}

Interaction Tests

#[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);
}

Performance Targets

  • Frame rate: 60 FPS constant
  • Input latency: < 16ms
  • Texture memory: < 10 MB for typical skin
  • CPU usage: < 5% idle, < 10% during playback

Example Usage

// 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 Classic Reference Layout

┌───────────────────────────────────────────────────────────┐
│ [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

Next Steps After Phase 2

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.