use std::sync::mpsc; use std::time::{Duration, Instant}; use std::collections::HashMap; use client_domain::{ BoundingBox, Color, DisplayPort, FontMetrics, RenderEngine, ScrollState, ThemeConfig, }; use client_application::{ClientApp, RepaintCommand}; use domain::{DisplayHint, Value}; use protocol::ServerMessage; use super::RenderEvent; use crate::config::RENDER_POLL_INTERVAL; use crate::adapters::display::Esp32DisplayAdapter; use log::*; const SCROLL_TICK: Duration = Duration::from_millis(50); const INDICATOR_DIAMETER: u16 = 8; const INDICATOR_MARGIN: u16 = 4; const COLOR_CONNECTED: Color = Color(0, 200, 0); const COLOR_DISCONNECTED: Color = Color(200, 0, 0); struct WidgetCache { hint: DisplayHint, data: Vec<(String, Value)>, bounds: BoundingBox, scroll: ScrollState, } pub fn run( screen: BoundingBox, mut display: Esp32DisplayAdapter, rx: mpsc::Receiver, ) { let metrics = FontMetrics { small: (6, 10), large: (10, 20), }; let mut engine = RenderEngine::new(metrics, ThemeConfig::default()); let mut app = ClientApp::new(screen); let mut widgets: HashMap = HashMap::new(); let mut first_update = true; let mut last_tick = Instant::now(); let mut connected = false; info!("Render loop started"); draw_indicator(&mut display, screen, connected); display.flush().unwrap(); loop { let timeout = RENDER_POLL_INTERVAL.min(SCROLL_TICK); match rx.recv_timeout(timeout) { Ok(RenderEvent::ConnectionStatus(status)) => { if status != connected { connected = status; draw_indicator(&mut display, screen, connected); display.flush().unwrap(); } } Ok(RenderEvent::Server(msg)) => { let is_screen_update = matches!(msg, ServerMessage::ScreenUpdate { .. }); let repaints = app.handle_message(msg); let theme_changed = app.take_theme_changed(); if theme_changed { engine.set_theme(app.theme().clone()); } let bg = engine.theme().background; if !repaints.is_empty() && (first_update || is_screen_update || theme_changed) { display.fill_rect(screen, bg).unwrap(); first_update = false; } for cmd in &repaints { let cache = update_cache(&engine, cmd); display.fill_rect(cache.bounds, bg).unwrap(); draw_widget(&engine, &mut display, &cache); widgets.insert(cmd.widget_id, cache); } if !repaints.is_empty() { draw_indicator(&mut display, screen, connected); display.flush().unwrap(); } } Err(mpsc::RecvTimeoutError::Timeout) => {} Err(mpsc::RecvTimeoutError::Disconnected) => { error!("Network thread died"); break; } } let now = Instant::now(); let elapsed = now.duration_since(last_tick); last_tick = now; let mut needs_flush = false; for cache in widgets.values_mut() { if cache.scroll.tick(elapsed) { let bg = engine.theme().background; display.fill_rect(cache.bounds, bg).unwrap(); draw_widget(&engine, &mut display, cache); needs_flush = true; } } if needs_flush { draw_indicator(&mut display, screen, connected); display.flush().unwrap(); } } } fn draw_indicator(display: &mut Esp32DisplayAdapter, screen: BoundingBox, connected: bool) { let color = if connected { COLOR_CONNECTED } else { COLOR_DISCONNECTED }; let x = screen.x + screen.width - INDICATOR_DIAMETER - INDICATOR_MARGIN; let y = screen.y + screen.height - INDICATOR_DIAMETER - INDICATOR_MARGIN; display.fill_circle(x, y, INDICATOR_DIAMETER, color).unwrap(); } fn update_cache(engine: &RenderEngine, cmd: &RepaintCommand) -> WidgetCache { let hint: DisplayHint = cmd.display_hint.clone().into(); let data: Vec<(String, Value)> = cmd.state.data .iter() .map(|kv| (kv.key.clone(), kv.value.clone().into())) .collect(); let content_h = engine.content_height(&hint, &data, cmd.bounds.width); let scroll = ScrollState::new(cmd.bounds.height, content_h); WidgetCache { hint, data, bounds: cmd.bounds, scroll, } } fn draw_widget( engine: &RenderEngine, display: &mut Esp32DisplayAdapter, cache: &WidgetCache, ) { let draw_cmds = engine.render_widget( &cache.hint, &cache.data, cache.bounds, cache.scroll.offset(), ); for dc in &draw_cmds { display.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font).unwrap(); } }