package rdp import ( "fmt" "math" "sync" "time" ) // MockBackend implements the RDPBackend interface for development and testing. // Instead of connecting to a real RDP server, it generates animated test frames // with a gradient pattern, moving shapes, and a timestamp overlay. type MockBackend struct { connected bool config RDPConfig buffer *PixelBuffer mu sync.Mutex startTime time.Time clipboard string } // NewMockBackend creates a new MockBackend instance. func NewMockBackend() *MockBackend { return &MockBackend{} } // Connect initializes the mock backend with the given configuration and starts // generating test frames. func (m *MockBackend) Connect(config RDPConfig) error { m.mu.Lock() defer m.mu.Unlock() if m.connected { return fmt.Errorf("already connected") } width := config.Width height := config.Height if width <= 0 { width = 1920 } if height <= 0 { height = 1080 } m.config = config m.config.Width = width m.config.Height = height m.buffer = NewPixelBuffer(width, height) m.connected = true m.startTime = time.Now() // Generate the initial test frame m.generateFrame() return nil } // Disconnect shuts down the mock backend. func (m *MockBackend) Disconnect() error { m.mu.Lock() defer m.mu.Unlock() if !m.connected { return fmt.Errorf("not connected") } m.connected = false m.buffer = nil return nil } // SendMouseEvent records a mouse event (no-op for mock). func (m *MockBackend) SendMouseEvent(x, y int, flags uint32) error { m.mu.Lock() defer m.mu.Unlock() if !m.connected { return fmt.Errorf("not connected") } // In the mock, we draw a cursor indicator at the mouse position if m.buffer != nil && flags&MouseFlagMove != 0 { m.drawCursor(x, y) } return nil } // SendKeyEvent records a key event (no-op for mock). func (m *MockBackend) SendKeyEvent(scancode uint32, pressed bool) error { m.mu.Lock() defer m.mu.Unlock() if !m.connected { return fmt.Errorf("not connected") } return nil } // SendClipboard stores clipboard text (mock implementation). func (m *MockBackend) SendClipboard(data string) error { m.mu.Lock() defer m.mu.Unlock() if !m.connected { return fmt.Errorf("not connected") } m.clipboard = data return nil } // GetFrame returns the current test frame. Each call regenerates the frame // with an updated animation state so the renderer can verify dynamic updates. func (m *MockBackend) GetFrame() ([]byte, error) { m.mu.Lock() defer m.mu.Unlock() if !m.connected { return nil, fmt.Errorf("not connected") } m.generateFrame() return m.buffer.GetFrame(), nil } // IsConnected reports whether the mock backend is connected. func (m *MockBackend) IsConnected() bool { m.mu.Lock() defer m.mu.Unlock() return m.connected } // generateFrame creates a visually interesting test frame with: // - A blue-to-purple diagonal gradient background // - An animated bouncing rectangle // - A grid overlay for alignment verification // - Session info text area func (m *MockBackend) generateFrame() { w := m.config.Width h := m.config.Height elapsed := time.Since(m.startTime).Seconds() data := make([]byte, w*h*4) // ── Background: diagonal gradient from dark blue to dark purple ── for y := 0; y < h; y++ { for x := 0; x < w; x++ { offset := (y*w + x) * 4 // Normalized coordinates nx := float64(x) / float64(w) ny := float64(y) / float64(h) diag := (nx + ny) / 2.0 r := uint8(20 + diag*40) // 20–60 g := uint8(25 + (1.0-diag)*30) // 25–55 b := uint8(80 + diag*100) // 80–180 data[offset+0] = r data[offset+1] = g data[offset+2] = b data[offset+3] = 255 } } // ── Grid overlay: subtle lines every 100 pixels ── for y := 0; y < h; y++ { for x := 0; x < w; x++ { if x%100 == 0 || y%100 == 0 { offset := (y*w + x) * 4 data[offset+0] = min8(data[offset+0]+20, 255) data[offset+1] = min8(data[offset+1]+20, 255) data[offset+2] = min8(data[offset+2]+20, 255) } } } // ── Animated bouncing rectangle ── rectW, rectH := 200, 120 // Bounce horizontally and vertically using sine/cosine cx := int(float64(w-rectW) * (0.5 + 0.4*math.Sin(elapsed*0.7))) cy := int(float64(h-rectH) * (0.5 + 0.4*math.Cos(elapsed*0.5))) // Color cycles through hues hue := math.Mod(elapsed*30, 360) rr, gg, bb := hueToRGB(hue) for ry := 0; ry < rectH; ry++ { for rx := 0; rx < rectW; rx++ { px := cx + rx py := cy + ry if px >= 0 && px < w && py >= 0 && py < h { offset := (py*w + px) * 4 // Border: 3px white outline if rx < 3 || rx >= rectW-3 || ry < 3 || ry >= rectH-3 { data[offset+0] = 255 data[offset+1] = 255 data[offset+2] = 255 data[offset+3] = 255 } else { data[offset+0] = rr data[offset+1] = gg data[offset+2] = bb data[offset+3] = 220 } } } } // ── Info panel in top-left corner ── panelW, panelH := 320, 80 for py := 10; py < 10+panelH && py < h; py++ { for px := 10; px < 10+panelW && px < w; px++ { offset := (py*w + px) * 4 data[offset+0] = 0 data[offset+1] = 0 data[offset+2] = 0 data[offset+3] = 180 } } // ── Secondary animated element: pulsing circle ── circleX := w / 2 circleY := h / 2 radius := 40.0 + 20.0*math.Sin(elapsed*2.0) for dy := -70; dy <= 70; dy++ { for dx := -70; dx <= 70; dx++ { dist := math.Sqrt(float64(dx*dx + dy*dy)) if dist <= radius && dist >= radius-4 { px := circleX + dx py := circleY + dy if px >= 0 && px < w && py >= 0 && py < h { offset := (py*w + px) * 4 data[offset+0] = 88 data[offset+1] = 166 data[offset+2] = 255 data[offset+3] = 255 } } } } // Apply as a full-frame update m.buffer.Update(0, 0, w, h, data) } // drawCursor draws a small crosshair at the given position. func (m *MockBackend) drawCursor(cx, cy int) { size := 10 w := m.config.Width h := m.config.Height cursorData := make([]byte, (size*2+1)*(size*2+1)*4) idx := 0 for dy := -size; dy <= size; dy++ { for dx := -size; dx <= size; dx++ { if dx == 0 || dy == 0 { cursorData[idx+0] = 255 cursorData[idx+1] = 255 cursorData[idx+2] = 0 cursorData[idx+3] = 200 } idx += 4 } } startX := cx - size startY := cy - size if startX < 0 { startX = 0 } if startY < 0 { startY = 0 } _ = w _ = h m.buffer.Update(startX, startY, size*2+1, size*2+1, cursorData) } // hueToRGB converts a hue angle (0-360) to RGB values. func hueToRGB(hue float64) (uint8, uint8, uint8) { h := math.Mod(hue, 360) / 60 c := 200.0 // chroma x := c * (1 - math.Abs(math.Mod(h, 2)-1)) var r, g, b float64 switch { case h < 1: r, g, b = c, x, 0 case h < 2: r, g, b = x, c, 0 case h < 3: r, g, b = 0, c, x case h < 4: r, g, b = 0, x, c case h < 5: r, g, b = x, 0, c default: r, g, b = c, 0, x } return uint8(r + 55), uint8(g + 55), uint8(b + 55) } // min8 returns the smaller of two uint8 values. func min8(a, b uint8) uint8 { if a < b { return a } return b }