DataSourceConfig refactored to enum: External/Clock/StaticText. Clock generates formatted time via chrono, static text emits configured string. ESP32: connection status indicator (green/red dot bottom-right), per-widget clear before redraw, RenderEvent enum for local + server messages. Polling uses DataUpdate instead of ScreenUpdate to avoid wiping widget state. Empty mappings passthrough raw source data for internal sources.
152 lines
4.9 KiB
Rust
152 lines
4.9 KiB
Rust
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<RenderEvent>,
|
|
) {
|
|
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<u16, WidgetCache> = 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();
|
|
}
|
|
}
|