Server: ThemeConfig entity + CRUD (GET/PUT /theme), SQLite persistence, ThemeUpdate broadcast to ESP32 on save and initial connect. Client: render engine uses theme colors, full-screen redraw on theme change. SPA: theme page with color pickers + presets, layout preview with TS port of layout engine, justify/align controls on containers. DisplayHint refactored to struct (kind + h_align + v_align).
96 lines
2.7 KiB
Rust
96 lines
2.7 KiB
Rust
use crate::error::TcpServerError;
|
|
use domain::{BroadcastPort, DisplayHint, Layout, ThemeConfig, WidgetId, WidgetState};
|
|
use protocol::{ServerMessage, WidgetDescriptor, WireColor, WireLayoutNode, WireTheme, encode};
|
|
use tokio::sync::broadcast;
|
|
|
|
pub struct TcpBroadcaster {
|
|
tx: broadcast::Sender<Vec<u8>>,
|
|
}
|
|
|
|
impl TcpBroadcaster {
|
|
pub fn new(capacity: usize) -> Self {
|
|
let (tx, _) = broadcast::channel(capacity);
|
|
Self { tx }
|
|
}
|
|
|
|
pub fn subscribe(&self) -> broadcast::Receiver<Vec<u8>> {
|
|
self.tx.subscribe()
|
|
}
|
|
|
|
fn send_frame(&self, frame: Vec<u8>) -> Result<(), TcpServerError> {
|
|
let _ = self.tx.send(frame);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl BroadcastPort for TcpBroadcaster {
|
|
type Error = TcpServerError;
|
|
|
|
async fn push_screen_update(
|
|
&self,
|
|
layout: &Layout,
|
|
widgets: &[(WidgetId, DisplayHint, WidgetState)],
|
|
) -> Result<(), Self::Error> {
|
|
let wire_layout: WireLayoutNode = (&layout.root).into();
|
|
let wire_widgets: Vec<WidgetDescriptor> = widgets
|
|
.iter()
|
|
.map(|(id, hint, state)| WidgetDescriptor {
|
|
id: *id,
|
|
display_hint: hint.into(),
|
|
state: state.into(),
|
|
})
|
|
.collect();
|
|
|
|
let msg = ServerMessage::ScreenUpdate {
|
|
layout: wire_layout,
|
|
widgets: wire_widgets,
|
|
};
|
|
|
|
let frame = encode(&msg).map_err(TcpServerError::Encode)?;
|
|
self.send_frame(frame)
|
|
}
|
|
|
|
async fn push_data_update(
|
|
&self,
|
|
updates: &[(WidgetId, DisplayHint, WidgetState)],
|
|
) -> Result<(), Self::Error> {
|
|
let wire_widgets: Vec<WidgetDescriptor> = updates
|
|
.iter()
|
|
.map(|(id, hint, state)| WidgetDescriptor {
|
|
id: *id,
|
|
display_hint: hint.into(),
|
|
state: state.into(),
|
|
})
|
|
.collect();
|
|
|
|
let msg = ServerMessage::DataUpdate {
|
|
widgets: wire_widgets,
|
|
};
|
|
|
|
let frame = encode(&msg).map_err(TcpServerError::Encode)?;
|
|
self.send_frame(frame)
|
|
}
|
|
|
|
async fn push_theme_update(&self, theme: &ThemeConfig) -> Result<(), Self::Error> {
|
|
let wire_theme = domain_theme_to_wire(theme);
|
|
let msg = ServerMessage::ThemeUpdate { theme: wire_theme };
|
|
let frame = encode(&msg).map_err(TcpServerError::Encode)?;
|
|
self.send_frame(frame)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn domain_theme_to_wire(t: &ThemeConfig) -> WireTheme {
|
|
let c = |c: &domain::ThemeColor| WireColor {
|
|
r: c.r,
|
|
g: c.g,
|
|
b: c.b,
|
|
};
|
|
WireTheme {
|
|
primary: c(&t.primary),
|
|
secondary: c(&t.secondary),
|
|
accent: c(&t.accent),
|
|
text: c(&t.text),
|
|
background: c(&t.background),
|
|
}
|
|
}
|