diff --git a/.env.example b/.env.example index b8ecc0e..f39aad6 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,6 @@ KFRAME_DATABASE_URL=sqlite:kframe.db?mode=rwc KFRAME_TCP_ADDR=0.0.0.0:2699 KFRAME_HTTP_ADDR=0.0.0.0:3000 -KFRAME_POLL_INTERVAL_SECS=5 # Auth (required) JWT_SECRET=change-me-to-a-random-secret diff --git a/crates/adapters/http-api/src/lib.rs b/crates/adapters/http-api/src/lib.rs index 54052a5..82bcbe3 100644 --- a/crates/adapters/http-api/src/lib.rs +++ b/crates/adapters/http-api/src/lib.rs @@ -1,6 +1,7 @@ pub mod extractors; mod routes; +use application::ConfigService; use axum::Router; use domain::{ AuthPort, BroadcastPort, ClientRegistry, ConfigRepository, EventPublisher, PasswordHashPort, @@ -36,6 +37,18 @@ impl Clone for AppState { } } +impl AppState +where + C: ConfigRepository, + C::Error: std::fmt::Debug, + E: EventPublisher, + E::Error: std::fmt::Debug, +{ + pub fn config_service(&self) -> ConfigService<'_, C, E> { + ConfigService::new(self.config.as_ref(), self.events.as_ref()) + } +} + pub fn router(state: AppState) -> Router where C: ConfigRepository + UserRepository + WidgetStateCache + Send + Sync + 'static, diff --git a/crates/adapters/http-api/src/routes/data_sources.rs b/crates/adapters/http-api/src/routes/data_sources.rs index 0cd1818..202f534 100644 --- a/crates/adapters/http-api/src/routes/data_sources.rs +++ b/crates/adapters/http-api/src/routes/data_sources.rs @@ -1,7 +1,7 @@ use crate::AppState; use crate::extractors::AuthUser; use api_types::DataSourceDto; -use application::ConfigService; + use axum::{ extract::{Path, State}, http::StatusCode, @@ -65,7 +65,7 @@ where let source = body .into_domain() .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.create_data_source(source) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; @@ -87,7 +87,7 @@ where let source = body .into_domain() .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.update_data_source(source) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; @@ -105,7 +105,7 @@ where E: EventPublisher, E::Error: std::fmt::Debug, { - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.delete_data_source(id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; diff --git a/crates/adapters/http-api/src/routes/layout.rs b/crates/adapters/http-api/src/routes/layout.rs index 033f6be..5e7787d 100644 --- a/crates/adapters/http-api/src/routes/layout.rs +++ b/crates/adapters/http-api/src/routes/layout.rs @@ -1,7 +1,7 @@ use crate::AppState; use crate::extractors::AuthUser; use api_types::LayoutDto; -use application::ConfigService; + use axum::{extract::State, http::StatusCode, response::Json}; use domain::{ConfigRepository, EventPublisher}; @@ -39,7 +39,7 @@ where let layout = body .into_domain() .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.update_layout(layout) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; diff --git a/crates/adapters/http-api/src/routes/presets.rs b/crates/adapters/http-api/src/routes/presets.rs index 02f11b6..ecfd36d 100644 --- a/crates/adapters/http-api/src/routes/presets.rs +++ b/crates/adapters/http-api/src/routes/presets.rs @@ -1,7 +1,7 @@ use crate::AppState; use crate::extractors::AuthUser; use api_types::{CreatePresetDto, PresetDto}; -use application::ConfigService; + use axum::{ extract::{Path, State}, http::StatusCode, @@ -65,7 +65,7 @@ where let preset = body .into_domain() .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.save_preset(preset) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; @@ -83,7 +83,7 @@ where E: EventPublisher, E::Error: std::fmt::Debug, { - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.delete_preset(id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -101,7 +101,7 @@ where E: EventPublisher, E::Error: std::fmt::Debug, { - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.load_preset(id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; diff --git a/crates/adapters/http-api/src/routes/theme.rs b/crates/adapters/http-api/src/routes/theme.rs index 7179aa4..8aff736 100644 --- a/crates/adapters/http-api/src/routes/theme.rs +++ b/crates/adapters/http-api/src/routes/theme.rs @@ -1,7 +1,7 @@ use crate::AppState; use crate::extractors::AuthUser; use api_types::ThemeDto; -use application::ConfigService; + use axum::{extract::State, http::StatusCode, response::Json}; use domain::{ConfigRepository, EventPublisher}; @@ -38,7 +38,7 @@ where E::Error: std::fmt::Debug, { let theme = body.into_domain(); - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.update_theme(theme) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; diff --git a/crates/adapters/http-api/src/routes/widgets.rs b/crates/adapters/http-api/src/routes/widgets.rs index 4e4418b..3acd79b 100644 --- a/crates/adapters/http-api/src/routes/widgets.rs +++ b/crates/adapters/http-api/src/routes/widgets.rs @@ -1,7 +1,7 @@ use crate::AppState; use crate::extractors::AuthUser; use api_types::{CreateWidgetDto, WidgetDto}; -use application::ConfigService; + use axum::{ extract::{Path, State}, http::StatusCode, @@ -65,7 +65,7 @@ where let widget = body .into_domain() .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.create_widget(widget) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; @@ -87,7 +87,7 @@ where let widget = body .into_domain() .map_err(|e| (StatusCode::BAD_REQUEST, e))?; - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.update_widget(widget) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?; @@ -105,7 +105,7 @@ where E: EventPublisher, E::Error: std::fmt::Debug, { - let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref()); + let svc = state.config_service(); svc.delete_widget(id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; diff --git a/crates/application/src/event_service.rs b/crates/application/src/event_service.rs new file mode 100644 index 0000000..0ed4964 --- /dev/null +++ b/crates/application/src/event_service.rs @@ -0,0 +1,123 @@ +use crate::DataProjection; +use domain::{ + BroadcastPort, ConfigRepository, DomainEvent, Layout, Value, WidgetConfig, WidgetState, +}; +use std::sync::Arc; +use tracing::{error, info, warn}; + +pub async fn handle_event( + event: DomainEvent, + config: &Arc, + broadcaster: &Arc, + projection: &Arc, +) where + C: ConfigRepository, + C::Error: std::fmt::Display, + B: BroadcastPort, + B::Error: std::fmt::Display, +{ + match event { + DomainEvent::LayoutChanged { layout } => { + handle_layout_changed(&layout, config, broadcaster, projection).await; + } + DomainEvent::WebhookDataReceived { source_id, data } => { + handle_webhook_data(source_id, &data, config, broadcaster, projection).await; + } + DomainEvent::ThemeChanged { theme } => { + if let Err(e) = broadcaster.push_theme_update(&theme).await { + error!(error = %e, "failed to push theme update"); + } + info!("theme changed, pushed update to clients"); + } + _ => {} + } +} + +async fn handle_layout_changed( + layout: &Layout, + config: &Arc, + broadcaster: &Arc, + projection: &Arc, +) where + C: ConfigRepository, + C::Error: std::fmt::Display, + B: BroadcastPort, + B::Error: std::fmt::Display, +{ + let widgets = match config.list_widgets().await { + Ok(w) => w, + Err(e) => { + error!(error = %e, "failed to fetch widgets for screen update"); + return; + } + }; + + let mut widget_states = Vec::new(); + for w in &widgets { + if let Some(s) = projection.get_state(w.id).await { + widget_states.push((w.id, w.display_hint.clone(), s)); + } + } + + if let Err(e) = broadcaster.push_screen_update(layout, &widget_states).await { + error!(error = %e, "failed to push screen update"); + } + + info!("layout changed, pushed screen update to clients"); +} + +async fn handle_webhook_data( + source_id: u16, + data: &Value, + config: &Arc, + broadcaster: &Arc, + projection: &Arc, +) where + C: ConfigRepository, + C::Error: std::fmt::Display, + B: BroadcastPort, + B::Error: std::fmt::Display, +{ + let widgets = match config.list_widgets().await { + Ok(w) => w, + Err(e) => { + error!(error = %e, "failed to fetch widgets for webhook"); + return; + } + }; + + let changed = apply_and_broadcast(source_id, data, &widgets, broadcaster, projection).await; + if !changed.is_empty() { + info!(source_id, count = changed.len(), "webhook data pushed"); + } +} + +pub async fn apply_and_broadcast( + source_id: u16, + data: &Value, + widgets: &[WidgetConfig], + broadcaster: &Arc, + projection: &Arc, +) -> Vec<(u16, WidgetState)> +where + B: BroadcastPort, + B::Error: std::fmt::Display, +{ + let changed: Vec<(u16, WidgetState)> = + projection.apply_poll_result(source_id, data, widgets).await; + + if !changed.is_empty() { + let with_hints: Vec<_> = changed + .iter() + .filter_map(|(id, state)| { + let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone(); + Some((*id, hint, state.clone())) + }) + .collect(); + if let Err(e) = broadcaster.push_data_update(&with_hints).await { + warn!(error = %e, "failed to push update"); + } + } + + changed +} diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs index 99ed404..0ec23d5 100644 --- a/crates/application/src/lib.rs +++ b/crates/application/src/lib.rs @@ -1,6 +1,7 @@ pub mod auth_service; mod config_service; mod data_projection; +pub mod event_service; pub mod polling_service; pub use config_service::ConfigService; diff --git a/crates/application/src/polling_service.rs b/crates/application/src/polling_service.rs index 1d1ad73..89f8b4a 100644 --- a/crates/application/src/polling_service.rs +++ b/crates/application/src/polling_service.rs @@ -130,7 +130,20 @@ async fn poll_and_broadcast( return; } }; - broadcast_changes(source, &result, &widgets, broadcaster, projection, config).await; + let changed = crate::event_service::apply_and_broadcast( + source.id, + &result, + &widgets, + broadcaster, + projection, + ) + .await; + if !changed.is_empty() { + if let Err(e) = config.save_widget_states(&changed).await { + warn!(error = %e, "failed to cache widget states"); + } + info!(source = %source.name, count = changed.len(), "pushed widget updates"); + } } async fn poll_loop( @@ -176,55 +189,25 @@ async fn poll_loop( last_refresh = tokio::time::Instant::now(); } - broadcast_changes( - &source, + let changed = crate::event_service::apply_and_broadcast( + source.id, &result, &widgets, &broadcaster, &projection, - &config, ) .await; + if !changed.is_empty() { + if let Err(e) = config.save_widget_states(&changed).await { + warn!(error = %e, "failed to cache widget states"); + } + info!(source = %source.name, count = changed.len(), "pushed widget updates"); + } tokio::time::sleep(interval).await; } } -async fn broadcast_changes( - source: &DataSource, - result: &Value, - widgets: &[WidgetConfig], - broadcaster: &Arc, - projection: &Arc, - config: &Arc, -) where - C: WidgetStateCache, - C::Error: std::fmt::Display, - B: BroadcastPort, - B::Error: std::fmt::Display, -{ - let changed: Vec<(u16, WidgetState)> = projection - .apply_poll_result(source.id, result, widgets) - .await; - - if !changed.is_empty() { - let with_hints: Vec<_> = changed - .iter() - .filter_map(|(id, state)| { - let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone(); - Some((*id, hint, state.clone())) - }) - .collect(); - if let Err(e) = broadcaster.push_data_update(&with_hints).await { - warn!(error = %e, "failed to push update"); - } - if let Err(e) = config.save_widget_states(&changed).await { - warn!(error = %e, "failed to cache widget states"); - } - info!(source = %source.name, count = changed.len(), "pushed widget updates"); - } -} - async fn broadcast_errors( source: &DataSource, widgets: &[WidgetConfig], diff --git a/crates/bootstrap/src/config.rs b/crates/bootstrap/src/config.rs index 9e7645e..20c4b44 100644 --- a/crates/bootstrap/src/config.rs +++ b/crates/bootstrap/src/config.rs @@ -4,7 +4,6 @@ pub struct ServerConfig { pub database_url: String, pub tcp_addr: String, pub http_addr: String, - pub poll_interval_secs: u64, pub spa_dir: Option, } @@ -15,10 +14,6 @@ impl ServerConfig { .unwrap_or_else(|_| "sqlite:kframe.db?mode=rwc".into()), tcp_addr: env::var("KFRAME_TCP_ADDR").unwrap_or_else(|_| "0.0.0.0:2699".into()), http_addr: env::var("KFRAME_HTTP_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".into()), - poll_interval_secs: env::var("KFRAME_POLL_INTERVAL_SECS") - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(5), spa_dir: env::var("KFRAME_SPA_DIR").ok(), } } diff --git a/crates/bootstrap/src/event_handler.rs b/crates/bootstrap/src/event_handler.rs index ab164bf..8369510 100644 --- a/crates/bootstrap/src/event_handler.rs +++ b/crates/bootstrap/src/event_handler.rs @@ -1,9 +1,8 @@ use application::DataProjection; use config_sqlite::SqliteConfigStore; -use domain::{BroadcastPort, ConfigRepository, DomainEvent}; use std::sync::Arc; use tcp_server::{TcpBroadcaster, TcpEventBus}; -use tracing::{error, info, warn}; +use tracing::{error, warn}; pub async fn run( event_bus: Arc, @@ -15,69 +14,10 @@ pub async fn run( loop { match rx.recv().await { - Ok(DomainEvent::LayoutChanged { layout }) => { - let widgets = match config.list_widgets().await { - Ok(w) => w, - Err(e) => { - error!(error = %e, "failed to fetch widgets for screen update"); - continue; - } - }; - - let mut widget_states = Vec::new(); - for w in &widgets { - if let Some(s) = projection.get_state(w.id).await { - widget_states.push((w.id, w.display_hint.clone(), s)); - } - } - - if let Err(e) = broadcaster - .push_screen_update(&layout, &widget_states) - .await - { - error!(error = %e, "failed to push screen update"); - } - - info!("layout changed, pushed screen update to clients"); - } - Ok(DomainEvent::WebhookDataReceived { source_id, data }) => { - let widgets = match config.list_widgets().await { - Ok(w) => w, - Err(e) => { - error!(error = %e, "failed to fetch widgets for webhook"); - continue; - } - }; - - let changed = projection - .apply_poll_result(source_id, &data, &widgets) + Ok(event) => { + application::event_service::handle_event(event, &config, &broadcaster, &projection) .await; - - if !changed.is_empty() { - let with_hints: Vec<_> = changed - .iter() - .filter_map(|(id, state)| { - let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone(); - Some((*id, hint, state.clone())) - }) - .collect(); - if let Err(e) = broadcaster.push_data_update(&with_hints).await { - error!(error = %e, "failed to push webhook data update"); - } - info!( - source_id, - count = changed.len(), - "webhook data received, pushed update" - ); - } } - Ok(DomainEvent::ThemeChanged { theme }) => { - if let Err(e) = broadcaster.push_theme_update(&theme).await { - error!(error = %e, "failed to push theme update"); - } - info!("theme changed, pushed update to clients"); - } - Ok(_) => {} Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { warn!(skipped = n, "event handler lagged, missed events"); } diff --git a/crates/bootstrap/src/main.rs b/crates/bootstrap/src/main.rs index 58150b6..3a161b1 100644 --- a/crates/bootstrap/src/main.rs +++ b/crates/bootstrap/src/main.rs @@ -90,11 +90,5 @@ async fn main() -> Result<()> { event_handler::run(ev_bus, ev_config, ev_bc, ev_proj).await; }); - polling::run( - config_store, - broadcaster, - projection, - cfg.poll_interval_secs, - ) - .await + polling::run(config_store, broadcaster, projection).await } diff --git a/crates/bootstrap/src/polling.rs b/crates/bootstrap/src/polling.rs index 5c2d53e..ef9a868 100644 --- a/crates/bootstrap/src/polling.rs +++ b/crates/bootstrap/src/polling.rs @@ -57,7 +57,6 @@ pub async fn run( config: Arc, broadcaster: Arc, projection: Arc, - _poll_interval_secs: u64, ) -> Result<()> { let adapters = Adapters { http: Arc::new(HttpJsonAdapter::new()), diff --git a/crates/client-application/src/client_app.rs b/crates/client-application/src/client_app.rs index b89acc4..12c3736 100644 --- a/crates/client-application/src/client_app.rs +++ b/crates/client-application/src/client_app.rs @@ -1,24 +1,24 @@ -use crate::conversions::wire_to_layout; +use crate::conversions::{wire_to_display_hint, wire_to_layout, wire_to_widget_state}; use client_domain::{BoundingBox, Color, LayoutEngine, RenderTree, ThemeConfig}; -use protocol::{ - ServerMessage, WidgetDescriptor, WireColor, WireDisplayHint, WireLayoutNode, WireWidgetState, -}; +use domain::{DisplayHint, Value, WidgetError, WidgetState}; +use protocol::{ServerMessage, WidgetDescriptor, WireColor, WireLayoutNode}; use std::collections::HashMap; pub struct ClientApp { screen: BoundingBox, render_tree: Option, - widget_states: HashMap, + widget_states: HashMap, theme: ThemeConfig, theme_changed: bool, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct RepaintCommand { pub widget_id: u16, pub bounds: BoundingBox, - pub display_hint: WireDisplayHint, - pub state: WireWidgetState, + pub display_hint: DisplayHint, + pub data: Vec<(String, Value)>, + pub error: Option, } impl ClientApp { @@ -77,9 +77,10 @@ impl ClientApp { let new_tree = LayoutEngine::compute(&layout, self.screen); self.widget_states.clear(); - for w in &widgets { - self.widget_states - .insert(w.id, (w.display_hint.clone(), w.state.clone())); + for w in widgets { + let hint = wire_to_display_hint(w.display_hint); + let state = wire_to_widget_state(w.state); + self.widget_states.insert(w.id, (hint, state)); } let repaints = self.build_repaints_for_all(&new_tree); @@ -96,21 +97,19 @@ impl ClientApp { let mut repaints = Vec::new(); for w in widgets { + let hint = wire_to_display_hint(w.display_hint); + let state = wire_to_widget_state(w.state); + let changed = self .widget_states .get(&w.id) - .is_none_or(|(_, prev_state)| *prev_state != w.state); + .is_none_or(|(_, prev)| *prev != state); if changed { if let Some(bounds) = tree.get_widget_bounds(w.id) { - repaints.push(RepaintCommand { - widget_id: w.id, - bounds: *bounds, - display_hint: w.display_hint.clone(), - state: w.state.clone(), - }); + repaints.push(Self::make_repaint(w.id, *bounds, &hint, &state)); } - self.widget_states.insert(w.id, (w.display_hint, w.state)); + self.widget_states.insert(w.id, (hint, state)); } } @@ -122,18 +121,32 @@ impl ClientApp { for (id, (hint, state)) in &self.widget_states { if let Some(bounds) = tree.get_widget_bounds(*id) { - repaints.push(RepaintCommand { - widget_id: *id, - bounds: *bounds, - display_hint: hint.clone(), - state: state.clone(), - }); + repaints.push(Self::make_repaint(*id, *bounds, hint, state)); } } repaints.sort_by_key(|r| r.widget_id); repaints } + + fn make_repaint( + id: u16, + bounds: BoundingBox, + hint: &DisplayHint, + state: &WidgetState, + ) -> RepaintCommand { + RepaintCommand { + widget_id: id, + bounds, + display_hint: hint.clone(), + data: state + .data + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(), + error: state.error.clone(), + } + } } fn wire_color(c: WireColor) -> Color { diff --git a/crates/client-application/tests/client_app_tests.rs b/crates/client-application/tests/client_app_tests.rs index bc21eab..c3b8053 100644 --- a/crates/client-application/tests/client_app_tests.rs +++ b/crates/client-application/tests/client_app_tests.rs @@ -1,4 +1,4 @@ -use client_application::{ClientApp, RepaintCommand}; +use client_application::ClientApp; use client_domain::BoundingBox; use protocol::{ ServerMessage, WidgetDescriptor, WireAlignItems, WireContainerNode, WireDirection, @@ -84,8 +84,8 @@ fn data_update_only_repaints_changed_widgets() { assert_eq!(repaints.len(), 1); assert_eq!(repaints[0].widget_id, 1); assert_eq!( - repaints[0].state.data[0].value, - WireValue::String("6.1°C".into()) + repaints[0].data[0], + ("temperature".into(), domain::Value::String("6.1°C".into())) ); } diff --git a/crates/client-desktop/src/main.rs b/crates/client-desktop/src/main.rs index aa1d1cd..c614a90 100644 --- a/crates/client-desktop/src/main.rs +++ b/crates/client-desktop/src/main.rs @@ -1,4 +1,4 @@ -use client_application::{ClientApp, conversions, run_connection_loop}; +use client_application::{ClientApp, RepaintCommand, run_connection_loop}; use client_domain::{ BoundingBox, DisplayPort, FontMetrics, RenderEngine, RepaintRequest, ThemeConfig, WidgetRenderer, @@ -10,6 +10,16 @@ use std::thread; use std::time::{Duration, Instant}; use tcp_client::StdTcpClient; +fn to_request(cmd: &RepaintCommand) -> RepaintRequest { + RepaintRequest { + widget_id: cmd.widget_id, + bounds: cmd.bounds, + display_hint: cmd.display_hint.clone(), + data: cmd.data.clone(), + error: cmd.error.clone(), + } +} + fn main() { let screen = BoundingBox::screen(240, 320); let mut app = ClientApp::new(screen); @@ -56,30 +66,7 @@ fn main() { if !repaints.is_empty() { println!("\n--- Repaint ({} widgets) ---", repaints.len()); - let requests: Vec = repaints - .iter() - .map(|cmd| RepaintRequest { - widget_id: cmd.widget_id, - bounds: cmd.bounds, - display_hint: conversions::wire_to_display_hint( - cmd.display_hint.clone(), - ), - data: cmd - .state - .data - .iter() - .map(|kv| { - (kv.key.clone(), conversions::wire_to_value(kv.value.clone())) - }) - .collect(), - error: cmd - .state - .error - .as_ref() - .map(|e| conversions::wire_to_widget_error(e.clone())), - }) - .collect(); - + let requests: Vec<_> = repaints.iter().map(to_request).collect(); let bg = engine.theme().background; let updates = renderer.apply_repaints(&engine, requests); for update in &updates { diff --git a/crates/client-domain/src/lib.rs b/crates/client-domain/src/lib.rs index 15948da..d855b74 100644 --- a/crates/client-domain/src/lib.rs +++ b/crates/client-domain/src/lib.rs @@ -19,7 +19,7 @@ pub use domain::{AlignItems, DisplayHintKind, HAlign, JustifyContent, VAlign}; pub use font::{FontMetrics, FontSize}; pub use layout_engine::LayoutEngine; pub use markup::{TextSpan, parse_markup}; -pub use ports::{ClientConfig, DisplayPort, NetworkPort, StoragePort}; +pub use ports::{DisplayPort, NetworkPort}; pub use render_engine::{DrawCommand, RenderEngine}; pub use render_tree::RenderTree; pub use scroll::ScrollState; diff --git a/crates/client-domain/src/ports/mod.rs b/crates/client-domain/src/ports/mod.rs index 9e5b042..cf77f0c 100644 --- a/crates/client-domain/src/ports/mod.rs +++ b/crates/client-domain/src/ports/mod.rs @@ -1,7 +1,5 @@ mod display; mod network; -mod storage; pub use display::DisplayPort; pub use network::NetworkPort; -pub use storage::{ClientConfig, StoragePort}; diff --git a/crates/client-domain/src/ports/storage.rs b/crates/client-domain/src/ports/storage.rs deleted file mode 100644 index bb50017..0000000 --- a/crates/client-domain/src/ports/storage.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub struct ClientConfig { - pub wifi_ssid: String, - pub wifi_password: String, - pub server_addr: String, -} - -pub trait StoragePort { - type Error; - - fn load_config(&self) -> Result; -} diff --git a/crates/client-esp32/src/tasks/render.rs b/crates/client-esp32/src/tasks/render.rs index 8944f06..e8a4adb 100644 --- a/crates/client-esp32/src/tasks/render.rs +++ b/crates/client-esp32/src/tasks/render.rs @@ -1,10 +1,12 @@ use std::sync::mpsc; use std::time::{Duration, Instant}; + +use client_application::RepaintCommand; use client_domain::{ BoundingBox, Color, DisplayPort, FontMetrics, RenderEngine, RepaintRequest, ThemeConfig, WidgetRenderer, }; -use client_application::{ClientApp, RepaintCommand, conversions}; +use client_application::ClientApp; use protocol::ServerMessage; use super::RenderEvent; use crate::config::RENDER_POLL_INTERVAL; @@ -17,17 +19,14 @@ const INDICATOR_MARGIN: u16 = 4; const COLOR_CONNECTED: Color = Color(0, 200, 0); const COLOR_DISCONNECTED: Color = Color(200, 0, 0); -fn to_repaint_requests(repaints: &[RepaintCommand]) -> Vec { - repaints - .iter() - .map(|cmd| RepaintRequest { - widget_id: cmd.widget_id, - bounds: cmd.bounds, - display_hint: conversions::wire_to_display_hint(cmd.display_hint.clone()), - data: cmd.state.data.iter().map(|kv| (kv.key.clone(), conversions::wire_to_value(kv.value.clone()))).collect(), - error: cmd.state.error.as_ref().map(|e| conversions::wire_to_widget_error(e.clone())), - }) - .collect() +fn to_request(cmd: &RepaintCommand) -> RepaintRequest { + RepaintRequest { + widget_id: cmd.widget_id, + bounds: cmd.bounds, + display_hint: cmd.display_hint.clone(), + data: cmd.data.clone(), + error: cmd.error.clone(), + } } pub fn run( @@ -52,7 +51,11 @@ pub fn run( loop { let has_scrollers = renderer.has_active_scrollers(); - let timeout = if has_scrollers { SCROLL_TICK } else { RENDER_POLL_INTERVAL }; + let timeout = if has_scrollers { + SCROLL_TICK + } else { + RENDER_POLL_INTERVAL + }; match rx.recv_timeout(timeout) { Ok(RenderEvent::ConnectionStatus(status)) => { if status != connected { @@ -76,12 +79,14 @@ pub fn run( first_update = false; } - let requests = to_repaint_requests(&repaints); + let requests: Vec<_> = repaints.iter().map(to_request).collect(); let updates = renderer.apply_repaints(&engine, requests); for update in &updates { display.fill_rect(update.bounds, bg).unwrap(); for dc in &update.commands { - display.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font).unwrap(); + display + .draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font) + .unwrap(); } } @@ -107,7 +112,9 @@ pub fn run( for update in &scroll_updates { display.fill_rect(update.bounds, bg).unwrap(); for dc in &update.commands { - display.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font).unwrap(); + display + .draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font) + .unwrap(); } } draw_indicator(&mut display, screen, connected); @@ -117,8 +124,14 @@ pub fn run( } fn draw_indicator(display: &mut Esp32DisplayAdapter, screen: BoundingBox, connected: bool) { - let color = if connected { COLOR_CONNECTED } else { COLOR_DISCONNECTED }; + 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(); + display + .fill_circle(x, y, INDICATOR_DIAMETER, color) + .unwrap(); }