From e9e25e81f43b5b9b0721fc9d3766ddb9bd341b82 Mon Sep 17 00:00:00 2001 From: Seregon <109359355+seregonwar@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:50:07 +0100 Subject: [PATCH] Add control controls and snapshot coverage --- Cargo.toml | 9 +- crates/strato-core/benches/state.rs | 2 +- crates/strato-core/src/config.rs | 98 ++- crates/strato-core/src/error.rs | 123 +-- crates/strato-core/src/event.rs | 90 ++- crates/strato-core/src/hot_reload.rs | 49 +- crates/strato-core/src/layout.rs | 129 +-- crates/strato-core/src/logging.rs | 81 +- crates/strato-core/src/plugin.rs | 134 ++-- crates/strato-core/src/reactive.rs | 30 +- crates/strato-core/src/state.rs | 4 +- crates/strato-core/src/text.rs | 109 ++- crates/strato-core/src/theme.rs | 93 ++- crates/strato-core/src/types.rs | 128 ++- crates/strato-core/src/ui_node.rs | 33 +- crates/strato-core/src/vdom.rs | 77 +- crates/strato-core/src/widget.rs | 72 +- crates/strato-core/src/window.rs | 126 +-- crates/strato-macros/src/lib.rs | 152 ++-- crates/strato-platform/src/application.rs | 25 +- crates/strato-platform/src/desktop.rs | 97 ++- crates/strato-platform/src/event_loop.rs | 741 ++++++++++-------- crates/strato-platform/src/init.rs | 42 +- crates/strato-platform/src/lib.rs | 37 +- crates/strato-platform/src/web.rs | 54 +- crates/strato-platform/src/window.rs | 43 +- crates/strato-renderer/src/backend/mod.rs | 14 +- crates/strato-renderer/src/backend/wgpu.rs | 553 +++++++++---- crates/strato-renderer/src/batch.rs | 222 ++++-- crates/strato-renderer/src/buffer.rs | 352 +++++---- crates/strato-renderer/src/device.rs | 312 +++++--- crates/strato-renderer/src/font_config.rs | 92 ++- crates/strato-renderer/src/font_system.rs | 151 ++-- crates/strato-renderer/src/glyph_atlas.rs | 81 +- crates/strato-renderer/src/gpu/buffer_mgr.rs | 29 +- crates/strato-renderer/src/gpu/drawing.rs | 404 ++++++---- crates/strato-renderer/src/gpu/mod.rs | 10 +- .../strato-renderer/src/gpu/pipeline_mgr.rs | 24 +- .../src/gpu/render_pass_mgr.rs | 10 +- crates/strato-renderer/src/gpu/shader_mgr.rs | 9 +- crates/strato-renderer/src/gpu/surface.rs | 7 +- crates/strato-renderer/src/gpu/texture_mgr.rs | 94 ++- crates/strato-renderer/src/integration.rs | 250 +++--- crates/strato-renderer/src/lib.rs | 21 +- crates/strato-renderer/src/memory.rs | 302 ++++--- crates/strato-renderer/src/pipeline.rs | 45 +- crates/strato-renderer/src/resources.rs | 155 ++-- crates/strato-renderer/src/shader.rs | 325 ++++---- crates/strato-renderer/src/text.rs | 101 ++- crates/strato-renderer/src/texture.rs | 13 +- crates/strato-renderer/src/vertex.rs | 123 +-- crates/strato-widgets/src/animation.rs | 60 +- crates/strato-widgets/src/button.rs | 281 ++++--- crates/strato-widgets/src/checkbox.rs | 292 ++++--- crates/strato-widgets/src/container.rs | 123 +-- crates/strato-widgets/src/control.rs | 254 ++++++ crates/strato-widgets/src/dropdown.rs | 129 +-- crates/strato-widgets/src/grid.rs | 76 +- crates/strato-widgets/src/image.rs | 176 +++-- crates/strato-widgets/src/input.rs | 207 ++--- crates/strato-widgets/src/inspector.rs | 8 +- crates/strato-widgets/src/layout.rs | 89 ++- crates/strato-widgets/src/lib.rs | 6 +- crates/strato-widgets/src/prelude.rs | 13 +- crates/strato-widgets/src/registry.rs | 149 ++-- crates/strato-widgets/src/scroll_view.rs | 89 ++- crates/strato-widgets/src/slider.rs | 152 ++-- crates/strato-widgets/src/text.rs | 143 ++-- crates/strato-widgets/src/theme.rs | 95 ++- crates/strato-widgets/src/top_bar.rs | 65 +- crates/strato-widgets/src/widget.rs | 6 +- crates/strato-widgets/src/wrap.rs | 52 +- .../strato-widgets/tests/control_snapshots.rs | 124 +++ crates/strato-widgets/tests/macro_ast_test.rs | 55 +- .../snapshots/button_state_commands.snap | 15 + .../tests/snapshots/slider_commands.snap | 8 + docs/swiftui_parity.md | 40 + examples/control_gallery/Cargo.toml | 10 + examples/control_gallery/src/main.rs | 68 ++ src/lib.rs | 26 +- 80 files changed, 5643 insertions(+), 3445 deletions(-) create mode 100644 crates/strato-widgets/src/control.rs create mode 100644 crates/strato-widgets/tests/control_snapshots.rs create mode 100644 crates/strato-widgets/tests/snapshots/button_state_commands.snap create mode 100644 crates/strato-widgets/tests/snapshots/slider_commands.snap create mode 100644 docs/swiftui_parity.md create mode 100644 examples/control_gallery/Cargo.toml create mode 100644 examples/control_gallery/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 1d22b21..8a87da7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,11 @@ members = [ "examples/calculator", "examples/custom_init", "examples/complex_demo", - "examples/advanced_renderer", - "examples/comprehensive_test", - "examples/modern_dashboard", - "crates/strato-core", + "examples/advanced_renderer", + "examples/comprehensive_test", + "examples/modern_dashboard", + "examples/control_gallery", + "crates/strato-core", "crates/strato-renderer", "crates/strato-widgets", "crates/strato-platform", diff --git a/crates/strato-core/benches/state.rs b/crates/strato-core/benches/state.rs index 2926079..7148d46 100644 --- a/crates/strato-core/benches/state.rs +++ b/crates/strato-core/benches/state.rs @@ -31,4 +31,4 @@ fn bench_state_basic(c: &mut Criterion) { } criterion_group!(benches, bench_state_update, bench_state_basic); -criterion_main!(benches); \ No newline at end of file +criterion_main!(benches); diff --git a/crates/strato-core/src/config.rs b/crates/strato-core/src/config.rs index 364c7d4..19b88ca 100644 --- a/crates/strato-core/src/config.rs +++ b/crates/strato-core/src/config.rs @@ -1,8 +1,8 @@ //! Configuration system for StratoUI framework +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::sync::{Arc, RwLock, OnceLock}; -use serde::{Serialize, Deserialize}; +use std::sync::{Arc, OnceLock, RwLock}; /// Global configuration for StratoUI #[derive(Debug, Clone, Serialize, Deserialize)] @@ -59,7 +59,7 @@ pub struct DebugConfig { impl Default for StratoConfig { fn default() -> Self { let mut category_levels = HashMap::new(); - + // Set default levels for each category category_levels.insert("core".to_string(), "info".to_string()); category_levels.insert("renderer".to_string(), "info".to_string()); @@ -70,7 +70,7 @@ impl Default for StratoConfig { category_levels.insert("vulkan".to_string(), "warn".to_string()); // Reduce Vulkan noise category_levels.insert("text".to_string(), "error".to_string()); // Disable text debug by default category_levels.insert("layout".to_string(), "error".to_string()); // Disable layout debug by default - + Self { logging: LoggingConfig { category_levels, @@ -125,38 +125,38 @@ impl ConfigManager { config: Arc::new(RwLock::new(StratoConfig::default())), } } - + /// Create a configuration manager with custom config pub fn with_config(config: StratoConfig) -> Self { Self { config: Arc::new(RwLock::new(config)), } } - + /// Get the global configuration manager instance pub fn instance() -> Option<&'static ConfigManager> { CONFIG_MANAGER.get() } - + /// Get a copy of the current configuration pub fn get_config(&self) -> StratoConfig { self.config.read().unwrap().clone() } - + /// Update the configuration - pub fn update_config(&self, updater: F) - where + pub fn update_config(&self, updater: F) + where F: FnOnce(&mut StratoConfig), { let mut config = self.config.write().unwrap(); updater(&mut *config); } - + /// Get the current logging configuration pub fn get_logging_config(&self) -> LoggingConfig { self.config.read().unwrap().logging.clone() } - + /// Update logging configuration pub fn update_logging_config(&self, updater: F) where @@ -165,38 +165,48 @@ impl ConfigManager { let mut config = self.config.write().unwrap(); updater(&mut config.logging); } - + /// Enable or disable text debug logging pub fn set_text_debug(&self, enabled: bool) { self.update_logging_config(|logging| { logging.enable_text_debug = enabled; if enabled { - logging.category_levels.insert("text".to_string(), "debug".to_string()); + logging + .category_levels + .insert("text".to_string(), "debug".to_string()); } else { - logging.category_levels.insert("text".to_string(), "error".to_string()); + logging + .category_levels + .insert("text".to_string(), "error".to_string()); } }); } - + /// Enable or disable layout debug logging pub fn set_layout_debug(&self, enabled: bool) { self.update_logging_config(|logging| { logging.enable_layout_debug = enabled; if enabled { - logging.category_levels.insert("layout".to_string(), "debug".to_string()); + logging + .category_levels + .insert("layout".to_string(), "debug".to_string()); } else { - logging.category_levels.insert("layout".to_string(), "error".to_string()); + logging + .category_levels + .insert("layout".to_string(), "error".to_string()); } }); } - + /// Set log level for a specific category pub fn set_category_level(&self, category: &str, level: &str) { self.update_logging_config(|logging| { - logging.category_levels.insert(category.to_string(), level.to_string()); + logging + .category_levels + .insert(category.to_string(), level.to_string()); }); } - + /// Get log level for a specific category pub fn get_category_level(&self, category: &str) -> Option { let config = self.config.read().unwrap(); @@ -235,31 +245,49 @@ mod tests { #[test] fn test_default_config() { let config = StratoConfig::default(); - + assert!(!config.logging.enable_text_debug); assert!(!config.logging.enable_layout_debug); - + // Text and Layout should be disabled by default - assert_eq!(config.logging.category_levels.get("text"), Some(&"error".to_string())); - assert_eq!(config.logging.category_levels.get("layout"), Some(&"error".to_string())); - + assert_eq!( + config.logging.category_levels.get("text"), + Some(&"error".to_string()) + ); + assert_eq!( + config.logging.category_levels.get("layout"), + Some(&"error".to_string()) + ); + // Vulkan should be at Warn level to reduce noise - assert_eq!(config.logging.category_levels.get("vulkan"), Some(&"warn".to_string())); + assert_eq!( + config.logging.category_levels.get("vulkan"), + Some(&"warn".to_string()) + ); } - + #[test] fn test_config_manager() { let manager = ConfigManager::new(); - + // Test text debug toggle manager.set_text_debug(true); - assert_eq!(manager.get_category_level("text"), Some("debug".to_string())); - + assert_eq!( + manager.get_category_level("text"), + Some("debug".to_string()) + ); + manager.set_text_debug(false); - assert_eq!(manager.get_category_level("text"), Some("error".to_string())); - + assert_eq!( + manager.get_category_level("text"), + Some("error".to_string()) + ); + // Test category level setting manager.set_category_level("renderer", "trace"); - assert_eq!(manager.get_category_level("renderer"), Some("trace".to_string())); + assert_eq!( + manager.get_category_level("renderer"), + Some("trace".to_string()) + ); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/error.rs b/crates/strato-core/src/error.rs index b582257..c8fd8a3 100644 --- a/crates/strato-core/src/error.rs +++ b/crates/strato-core/src/error.rs @@ -1,7 +1,7 @@ //! Error types for StratoUI framework -use thiserror::Error; use std::collections::HashMap; +use thiserror::Error; /// Context information for errors to aid in debugging #[derive(Debug, Clone)] @@ -47,7 +47,8 @@ impl ErrorContext { ]; if !self.metadata.is_empty() { - let metadata_str = self.metadata + let metadata_str = self + .metadata .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() @@ -67,64 +68,64 @@ impl ErrorContext { #[derive(Debug, Error)] pub enum StratoError { #[error("Platform error: {message}")] - Platform { + Platform { message: String, context: Option, }, - + #[error("Renderer error: {message}")] - Renderer { + Renderer { message: String, context: Option, }, - + #[error("Widget error: {message}")] - Widget { + Widget { message: String, context: Option, }, - + #[error("State management error: {message}")] - State { + State { message: String, context: Option, }, - + #[error("Layout calculation error: {message}")] - Layout { + Layout { message: String, context: Option, }, - + #[error("Initialization error: {message}")] - Initialization { + Initialization { message: String, context: Option, }, - + #[error("Configuration error: {message}")] - Configuration { + Configuration { message: String, context: Option, }, - + #[error("IO error: {0}")] Io(#[from] std::io::Error), - + #[error("Not implemented: {message}")] - NotImplemented { + NotImplemented { message: String, context: Option, }, - + #[error("Plugin error: {message}")] - PluginError { + PluginError { message: String, context: Option, }, - + #[error("Other error: {message}")] - Other { + Other { message: String, context: Option, }, @@ -133,7 +134,7 @@ pub enum StratoError { impl StratoError { /// Create a platform error with context pub fn platform_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Platform { + Self::Platform { message: msg.into(), context: Some(context), } @@ -141,7 +142,7 @@ impl StratoError { /// Create a renderer error with context pub fn renderer_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Renderer { + Self::Renderer { message: msg.into(), context: Some(context), } @@ -149,7 +150,7 @@ impl StratoError { /// Create a widget error with context pub fn widget_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Widget { + Self::Widget { message: msg.into(), context: Some(context), } @@ -157,7 +158,7 @@ impl StratoError { /// Create a state error with context pub fn state_with_context>(msg: S, context: ErrorContext) -> Self { - Self::State { + Self::State { message: msg.into(), context: Some(context), } @@ -165,7 +166,7 @@ impl StratoError { /// Create a layout error with context pub fn layout_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Layout { + Self::Layout { message: msg.into(), context: Some(context), } @@ -173,7 +174,7 @@ impl StratoError { /// Create an initialization error with context pub fn initialization_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Initialization { + Self::Initialization { message: msg.into(), context: Some(context), } @@ -181,7 +182,7 @@ impl StratoError { /// Create a configuration error with context pub fn configuration_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Configuration { + Self::Configuration { message: msg.into(), context: Some(context), } @@ -189,7 +190,7 @@ impl StratoError { /// Create a plugin error with context pub fn plugin_with_context>(msg: S, context: ErrorContext) -> Self { - Self::PluginError { + Self::PluginError { message: msg.into(), context: Some(context), } @@ -197,7 +198,7 @@ impl StratoError { /// Create an other error with context pub fn other_with_context>(msg: S, context: ErrorContext) -> Self { - Self::Other { + Self::Other { message: msg.into(), context: Some(context), } @@ -205,79 +206,79 @@ impl StratoError { /// Create a platform error from a string pub fn platform>(msg: S) -> Self { - Self::Platform { + Self::Platform { message: msg.into(), context: None, } } - + /// Create a renderer error from a string pub fn renderer>(msg: S) -> Self { - Self::Renderer { + Self::Renderer { message: msg.into(), context: None, } } - + /// Create a widget error from a string pub fn widget>(msg: S) -> Self { - Self::Widget { + Self::Widget { message: msg.into(), context: None, } } - + /// Create a state error from a string pub fn state>(msg: S) -> Self { - Self::State { + Self::State { message: msg.into(), context: None, } } - + /// Create a layout error from a string pub fn layout>(msg: S) -> Self { - Self::Layout { + Self::Layout { message: msg.into(), context: None, } } - + /// Create an initialization error from a string pub fn initialization>(msg: S) -> Self { - Self::Initialization { + Self::Initialization { message: msg.into(), context: None, } } - + /// Create a configuration error from a string pub fn configuration>(msg: S) -> Self { - Self::Configuration { + Self::Configuration { message: msg.into(), context: None, } } - + /// Create a not implemented error from a string pub fn not_implemented>(msg: S) -> Self { - Self::NotImplemented { + Self::NotImplemented { message: msg.into(), context: None, } } - + /// Create a plugin error from a string pub fn plugin>(msg: S) -> Self { - Self::PluginError { + Self::PluginError { message: msg.into(), context: None, } } - + /// Create an other error from a string pub fn other>(msg: S) -> Self { - Self::Other { + Self::Other { message: msg.into(), context: None, } @@ -286,16 +287,16 @@ impl StratoError { /// Get the error context if available pub fn context(&self) -> Option<&ErrorContext> { match self { - Self::Platform { context, .. } | - Self::Renderer { context, .. } | - Self::Widget { context, .. } | - Self::State { context, .. } | - Self::Layout { context, .. } | - Self::Initialization { context, .. } | - Self::Configuration { context, .. } | - Self::NotImplemented { context, .. } | - Self::PluginError { context, .. } | - Self::Other { context, .. } => context.as_ref(), + Self::Platform { context, .. } + | Self::Renderer { context, .. } + | Self::Widget { context, .. } + | Self::State { context, .. } + | Self::Layout { context, .. } + | Self::Initialization { context, .. } + | Self::Configuration { context, .. } + | Self::NotImplemented { context, .. } + | Self::PluginError { context, .. } + | Self::Other { context, .. } => context.as_ref(), Self::Io(_) => None, } } @@ -315,4 +316,4 @@ impl StratoError { pub type Result = std::result::Result; /// Alternative result type alias for backward compatibility -pub type StratoResult = Result; \ No newline at end of file +pub type StratoResult = Result; diff --git a/crates/strato-core/src/event.rs b/crates/strato-core/src/event.rs index ff4bdc3..2c01bd8 100644 --- a/crates/strato-core/src/event.rs +++ b/crates/strato-core/src/event.rs @@ -1,9 +1,9 @@ //! Event handling system for StratoUI +use glam::Vec2; use std::any::Any; -use std::sync::Arc; use std::fmt::Debug; -use glam::Vec2; +use std::sync::Arc; /// Result of event handling #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -27,17 +27,77 @@ pub enum MouseButton { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyCode { // Letters - A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, // Numbers - Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, + Num0, + Num1, + Num2, + Num3, + Num4, + Num5, + Num6, + Num7, + Num8, + Num9, // Function keys - F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, // Control keys - Enter, Escape, Backspace, Tab, Space, - Left, Right, Up, Down, - Shift, Control, Alt, Super, + Enter, + Escape, + Backspace, + Tab, + Space, + Left, + Right, + Up, + Down, + Shift, + Control, + Alt, + Super, // Special - Delete, Insert, Home, End, PageUp, PageDown, + Delete, + Insert, + Home, + End, + PageUp, + PageDown, } /// High-level key event for text input and navigation @@ -130,17 +190,17 @@ pub enum Event { MouseEnter, /// Mouse left widget MouseExit, - + /// Key pressed KeyDown(KeyboardEvent), /// Key released KeyUp(KeyboardEvent), /// Text input TextInput(String), - + /// Window event Window(WindowEvent), - + /// Touch started TouchStart(TouchEvent), /// Touch moved @@ -149,7 +209,7 @@ pub enum Event { TouchEnd(TouchEvent), /// Touch cancelled TouchCancel(TouchEvent), - + /// Custom user event Custom(Arc), } @@ -158,7 +218,7 @@ pub enum Event { pub trait EventHandler: Send + Sync { /// Handle an event fn handle(&mut self, event: &Event) -> EventResult; - + /// Check if handler can handle this event type fn can_handle(&self, _event: &Event) -> bool { true @@ -256,7 +316,7 @@ mod tests { fn test_event_dispatcher() { let mut dispatcher = EventDispatcher::new(); dispatcher.add_handler(Box::new(TestHandler { handled_count: 0 })); - + let event = Event::MouseEnter; let result = dispatcher.dispatch(&event); assert_eq!(result, EventResult::Handled); diff --git a/crates/strato-core/src/hot_reload.rs b/crates/strato-core/src/hot_reload.rs index ae1661d..32111e9 100644 --- a/crates/strato-core/src/hot_reload.rs +++ b/crates/strato-core/src/hot_reload.rs @@ -3,18 +3,18 @@ //! Provides file watching, code reloading, and live preview capabilities //! for rapid development and iteration +use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::time::SystemTime; -use serde::{Serialize, Deserialize}; #[cfg(feature = "hot-reload")] -use notify::{Watcher, RecursiveMode, Event, EventKind, RecommendedWatcher}; +use futures_util::{SinkExt, StreamExt}; +#[cfg(feature = "hot-reload")] +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; #[cfg(feature = "hot-reload")] use tokio::sync::mpsc; #[cfg(feature = "hot-reload")] use tokio_tungstenite::{accept_async, tungstenite::Message}; -#[cfg(feature = "hot-reload")] -use futures_util::{SinkExt, StreamExt}; /// File change event types #[derive(Debug, Clone, PartialEq, Eq)] @@ -76,10 +76,10 @@ impl Default for HotReloadConfig { pub trait HotReloadHandler: Send + Sync { /// Handle file changes fn handle_change(&self, change: &FileChange) -> Result<(), Box>; - + /// Handle compilation errors fn handle_error(&self, error: &str); - + /// Handle successful reload fn handle_reload_success(&self); } @@ -126,16 +126,15 @@ impl FileWatcher { let watch_extensions = self.config.watch_extensions.clone(); // Create file watcher - let mut watcher = notify::recommended_watcher(move |res: Result| { - match res { + let mut watcher = + notify::recommended_watcher(move |res: Result| match res { Ok(event) => { if let Err(e) = tx.blocking_send(event) { eprintln!("Failed to send file event: {}", e); } } Err(e) => eprintln!("File watcher error: {}", e), - } - })?; + })?; // Watch configured directories for dir in &self.config.watch_dirs { @@ -157,7 +156,8 @@ impl FileWatcher { &debounce_cache, debounce_duration, &watch_extensions, - ).await; + ) + .await; } }); @@ -273,13 +273,10 @@ impl LivePreviewServer { } /// Run the WebSocket server - async fn run_server( - port: u16, - clients: Arc>>>, - ) { + async fn run_server(port: u16, clients: Arc>>>) { + use futures_util::{SinkExt, StreamExt}; use tokio::net::TcpListener; use tokio_tungstenite::{accept_async, tungstenite::Message}; - use futures_util::{SinkExt, StreamExt}; let addr = format!("127.0.0.1:{}", port); let listener = match TcpListener::bind(&addr).await { @@ -292,7 +289,7 @@ impl LivePreviewServer { while let Ok((stream, _)) = listener.accept().await { let clients = Arc::clone(&clients); - + tokio::spawn(async move { let ws_stream = match accept_async(stream).await { Ok(ws) => ws, @@ -324,7 +321,7 @@ impl LivePreviewServer { _ => {} } } - + // Remove client when disconnected clients_clone.write().clear(); // Simplified cleanup }); @@ -539,10 +536,16 @@ mod tests { #[test] fn test_should_watch_file() { let extensions = vec!["rs".to_string(), "toml".to_string()]; - + assert!(utils::should_watch_file(Path::new("main.rs"), &extensions)); - assert!(utils::should_watch_file(Path::new("Cargo.toml"), &extensions)); - assert!(!utils::should_watch_file(Path::new("README.md"), &extensions)); + assert!(utils::should_watch_file( + Path::new("Cargo.toml"), + &extensions + )); + assert!(!utils::should_watch_file( + Path::new("README.md"), + &extensions + )); } #[tokio::test] @@ -566,8 +569,8 @@ mod tests { async fn test_hot_reload_manager() { let mut config = HotReloadConfig::default(); config.enabled = false; // Disable for testing - + let manager = HotReloadManager::new(config); assert!(manager.is_ok()); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/layout.rs b/crates/strato-core/src/layout.rs index ddf9828..70c695d 100644 --- a/crates/strato-core/src/layout.rs +++ b/crates/strato-core/src/layout.rs @@ -1,5 +1,5 @@ //! Flexbox-based layout engine for StratoUI -//! +//! //! This module provides a comprehensive flexbox layout system that supports //! all major flexbox properties including direction, wrap, alignment, and gaps. @@ -59,8 +59,10 @@ impl Constraints { /// Check if a size satisfies these constraints pub fn is_satisfied_by(&self, size: Size) -> bool { - size.width >= self.min_width && size.width <= self.max_width && - size.height >= self.min_height && size.height <= self.max_height + size.width >= self.min_width + && size.width <= self.max_width + && size.height >= self.min_height + && size.height <= self.max_height } } @@ -116,7 +118,10 @@ impl FlexDirection { /// Check if this is reversed pub fn is_reverse(&self) -> bool { - matches!(self, FlexDirection::RowReverse | FlexDirection::ColumnReverse) + matches!( + self, + FlexDirection::RowReverse | FlexDirection::ColumnReverse + ) } } @@ -324,13 +329,20 @@ impl Layout { /// Get the bounds as (x, y, width, height) pub fn bounds(&self) -> (f32, f32, f32, f32) { - (self.position.x, self.position.y, self.size.width, self.size.height) + ( + self.position.x, + self.position.y, + self.size.width, + self.size.height, + ) } /// Check if a point is within this layout pub fn contains(&self, point: Vec2) -> bool { - point.x >= self.position.x && point.x <= self.position.x + self.size.width && - point.y >= self.position.y && point.y <= self.position.y + self.size.height + point.x >= self.position.x + && point.x <= self.position.x + self.size.width + && point.y >= self.position.y + && point.y <= self.position.y + self.size.height } } @@ -378,26 +390,27 @@ impl LayoutEngine { // Determine main and cross axis dimensions let (main_size, cross_size) = if container.direction.is_row() { - (content_constraints.max_width, content_constraints.max_height) + ( + content_constraints.max_width, + content_constraints.max_height, + ) } else { - (content_constraints.max_height, content_constraints.max_width) + ( + content_constraints.max_height, + content_constraints.max_width, + ) }; // Create flex lines let lines = self.create_flex_lines(container, children, main_size); - + // Calculate layouts for each line let mut layouts = Vec::with_capacity(children.len()); let mut cross_position = container.padding.top; for line in &lines { - let line_layouts = self.calculate_line_layout( - container, - children, - line, - main_size, - cross_position, - ); + let line_layouts = + self.calculate_line_layout(container, children, line, main_size, cross_position); layouts.extend(line_layouts); cross_position += line.cross_size + container.gap.row; } @@ -438,9 +451,9 @@ impl LayoutEngine { }; // Check if we need to wrap - let needs_wrap = container.wrap != FlexWrap::NoWrap && - !current_line.items.is_empty() && - current_line.main_size + item_main_size + container.gap.column > main_size; + let needs_wrap = container.wrap != FlexWrap::NoWrap + && !current_line.items.is_empty() + && current_line.main_size + item_main_size + container.gap.column > main_size; if needs_wrap { lines.push(current_line); @@ -476,15 +489,11 @@ impl LayoutEngine { cross_position: f32, ) -> Vec { let mut layouts = Vec::new(); - + // Calculate flex grow/shrink - let total_flex_grow: f32 = line.items.iter() - .map(|&i| children[i].0.flex_grow) - .sum(); - - let total_flex_shrink: f32 = line.items.iter() - .map(|&i| children[i].0.flex_shrink) - .sum(); + let total_flex_grow: f32 = line.items.iter().map(|&i| children[i].0.flex_grow).sum(); + + let total_flex_shrink: f32 = line.items.iter().map(|&i| children[i].0.flex_shrink).sum(); // Calculate available space let used_space = line.main_size - container.gap.column * (line.items.len() - 1) as f32; @@ -492,7 +501,7 @@ impl LayoutEngine { // Distribute free space let mut main_position = container.padding.left; - + // Apply justify-content match container.justify_content { JustifyContent::FlexEnd => main_position += free_space, @@ -513,7 +522,7 @@ impl LayoutEngine { for (idx, &item_idx) in line.items.iter().enumerate() { let (item, size) = &children[item_idx]; - + // Calculate item main size with flex let mut item_main_size = if container.direction.is_row() { size.width @@ -549,11 +558,32 @@ impl LayoutEngine { let mut item_cross_position = cross_position; match align { - AlignItems::FlexEnd => item_cross_position += line.cross_size - (item_cross_size + if container.direction.is_row() { item.margin.vertical() } else { item.margin.horizontal() }), - AlignItems::Center => item_cross_position += (line.cross_size - (item_cross_size + if container.direction.is_row() { item.margin.vertical() } else { item.margin.horizontal() })) / 2.0, + AlignItems::FlexEnd => { + item_cross_position += line.cross_size + - (item_cross_size + + if container.direction.is_row() { + item.margin.vertical() + } else { + item.margin.horizontal() + }) + } + AlignItems::Center => { + item_cross_position += (line.cross_size + - (item_cross_size + + if container.direction.is_row() { + item.margin.vertical() + } else { + item.margin.horizontal() + })) + / 2.0 + } AlignItems::Stretch => { // Stretch to fill cross axis - let margin = if container.direction.is_row() { item.margin.vertical() } else { item.margin.horizontal() }; + let margin = if container.direction.is_row() { + item.margin.vertical() + } else { + item.margin.horizontal() + }; item_cross_size = (line.cross_size - margin).max(0.0); } _ => {} @@ -562,12 +592,18 @@ impl LayoutEngine { // Create layout based on direction let layout = if container.direction.is_row() { Layout::new( - Vec2::new(main_position + item.margin.left, item_cross_position + item.margin.top), + Vec2::new( + main_position + item.margin.left, + item_cross_position + item.margin.top, + ), Size::new(item_main_size, item_cross_size), ) } else { Layout::new( - Vec2::new(item_cross_position + item.margin.left, main_position + item.margin.top), + Vec2::new( + item_cross_position + item.margin.left, + main_position + item.margin.top, + ), Size::new(item_cross_size, item_main_size), ) }; @@ -576,10 +612,12 @@ impl LayoutEngine { // Update position for next item main_position += item_main_size + item.margin.horizontal() + container.gap.column; - + // Apply justify-content spacing match container.justify_content { - JustifyContent::SpaceBetween if line.items.len() > 1 && idx < line.items.len() - 1 => { + JustifyContent::SpaceBetween + if line.items.len() > 1 && idx < line.items.len() - 1 => + { main_position += free_space / (line.items.len() - 1) as f32; } JustifyContent::SpaceAround => { @@ -635,9 +673,7 @@ impl LayoutEngine { AlignContent::SpaceAround => { cross_offset + (free_cross_space / lines.len() as f32) * line_idx as f32 } - AlignContent::SpaceEvenly => { - cross_offset * (line_idx + 1) as f32 - } + AlignContent::SpaceEvenly => cross_offset * (line_idx + 1) as f32, _ => cross_offset, }; @@ -684,20 +720,17 @@ mod tests { (FlexItem::grow(1.0), Size::new(0.0, 50.0)), (FlexItem::grow(2.0), Size::new(0.0, 50.0)), ]; - + let container = FlexContainer { direction: FlexDirection::Row, justify_content: JustifyContent::FlexStart, align_items: AlignItems::FlexStart, ..Default::default() }; - - let layouts = engine.calculate_flex_layout( - &container, - &children, - Constraints::loose(300.0, 100.0), - ); - + + let layouts = + engine.calculate_flex_layout(&container, &children, Constraints::loose(300.0, 100.0)); + assert_eq!(layouts.len(), 2); assert_eq!(layouts[0].size.width, 100.0); assert_eq!(layouts[1].size.width, 200.0); diff --git a/crates/strato-core/src/logging.rs b/crates/strato-core/src/logging.rs index dcd5b59..77f0ed5 100644 --- a/crates/strato-core/src/logging.rs +++ b/crates/strato-core/src/logging.rs @@ -3,11 +3,11 @@ //! This module provides a comprehensive logging system with rate limiting, //! contextual error information, and category-based filtering. +use crate::config::LoggingConfig; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::sync::{Arc, RwLock, OnceLock}; +use std::sync::{Arc, OnceLock, RwLock}; use std::time::{Duration, Instant}; -use serde::{Serialize, Deserialize}; -use crate::config::LoggingConfig; /// Log levels supported by the system #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] @@ -70,7 +70,7 @@ impl LogLevel { _ => None, } } - + /// Convert LogLevel to string pub fn as_str(&self) -> &'static str { match self { @@ -107,16 +107,16 @@ impl RateLimitState { duration, } } - + fn should_allow(&mut self) -> bool { let now = Instant::now(); - + // Reset counter if duration has passed if now.duration_since(self.last_reset) >= self.duration { self.last_reset = now; self.count = 0; } - + if self.count < self.max_count { self.count += 1; true @@ -141,21 +141,21 @@ impl LoggerConfig { config, } } - + /// Check if a log message should be allowed based on rate limiting pub fn should_allow_log(&self, category: &str) -> bool { let mut limiters = self.rate_limiters.write().unwrap(); - + let limiter = limiters.entry(category.to_string()).or_insert_with(|| { RateLimitState::new( self.config.max_rate_limit_count, Duration::from_secs(self.config.rate_limit_seconds), ) }); - + limiter.should_allow() } - + /// Check if a log level is enabled for a category pub fn is_level_enabled(&self, category: &str, level: LogLevel) -> bool { if let Some(category_level_str) = self.config.category_levels.get(category) { @@ -163,11 +163,11 @@ impl LoggerConfig { return level >= category_level; } } - + // Default to Info level if category not found level >= LogLevel::Info } - + /// Update the configuration pub fn update_config(&mut self, config: LoggingConfig) { self.config = config; @@ -195,25 +195,36 @@ fn get_logger() -> Option>> { pub fn log_internal(level: LogLevel, category: &str, message: &str, rate_limited: bool) { if let Some(logger) = get_logger() { let logger_guard = logger.read().unwrap(); - + // Check if level is enabled for this category if !logger_guard.is_level_enabled(category, level) { return; } - + // Check rate limiting if requested if rate_limited && !logger_guard.should_allow_log(category) { return; } - + drop(logger_guard); // Release the lock before printing - + // Format and print the log message let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"); - println!("[{}] [{}] [{}] {}", timestamp, level.as_str().to_uppercase(), category, message); + println!( + "[{}] [{}] [{}] {}", + timestamp, + level.as_str().to_uppercase(), + category, + message + ); } else { // Fallback if logger not initialized - println!("[UNINITIALIZED] [{}] [{}] {}", level.as_str().to_uppercase(), category, message); + println!( + "[UNINITIALIZED] [{}] [{}] {}", + level.as_str().to_uppercase(), + category, + message + ); } } @@ -306,17 +317,17 @@ macro_rules! strato_text_debug { } // Re-export macros for easier use -pub use strato_trace; pub use strato_debug; -pub use strato_info; -pub use strato_warn; -pub use strato_error; -pub use strato_trace_rate_limited; pub use strato_debug_rate_limited; -pub use strato_info_rate_limited; -pub use strato_warn_rate_limited; +pub use strato_error; pub use strato_error_rate_limited; +pub use strato_info; +pub use strato_info_rate_limited; pub use strato_text_debug; +pub use strato_trace; +pub use strato_trace_rate_limited; +pub use strato_warn; +pub use strato_warn_rate_limited; #[cfg(test)] mod tests { @@ -328,7 +339,7 @@ mod tests { assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info)); assert_eq!(LogLevel::from_str("INFO"), Some(LogLevel::Info)); assert_eq!(LogLevel::from_str("invalid"), None); - + assert_eq!(LogLevel::Info.as_str(), "info"); assert_eq!(LogLevel::Error.as_str(), "error"); } @@ -336,14 +347,14 @@ mod tests { #[test] fn test_rate_limiting() { let mut state = RateLimitState::new(2, Duration::from_millis(100)); - + // First two should be allowed assert!(state.should_allow()); assert!(state.should_allow()); - + // Third should be blocked assert!(!state.should_allow()); - + // After waiting, should be allowed again std::thread::sleep(Duration::from_millis(150)); assert!(state.should_allow()); @@ -353,7 +364,7 @@ mod tests { fn test_logger_config() { let mut category_levels = HashMap::new(); category_levels.insert("test".to_string(), "debug".to_string()); - + let config = LoggingConfig { category_levels, enable_text_debug: true, @@ -361,15 +372,15 @@ mod tests { rate_limit_seconds: 1, max_rate_limit_count: 5, }; - + let logger_config = LoggerConfig::new(config); - + // Test level checking assert!(logger_config.is_level_enabled("test", LogLevel::Debug)); assert!(logger_config.is_level_enabled("test", LogLevel::Error)); assert!(!logger_config.is_level_enabled("test", LogLevel::Trace)); - + // Test rate limiting assert!(logger_config.should_allow_log("test")); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/plugin.rs b/crates/strato-core/src/plugin.rs index 98c9387..910ffd5 100644 --- a/crates/strato-core/src/plugin.rs +++ b/crates/strato-core/src/plugin.rs @@ -3,18 +3,18 @@ //! Provides a flexible plugin architecture for extending framework functionality //! with custom widgets, themes, and behaviors. +use crate::{ + error::{Result, StratoError}, + event::{Event, EventHandler, EventResult}, + theme::Theme, + widget::Widget, +}; +use serde::{Deserialize, Serialize}; use std::{ any::Any, collections::HashMap, sync::{Arc, RwLock}, }; -use serde::{Deserialize, Serialize}; -use crate::{ - error::{StratoError, Result}, - event::{Event, EventHandler, EventResult}, - widget::Widget, - theme::Theme, -}; /// Plugin metadata #[derive(Debug, Clone, Serialize, Deserialize)] @@ -124,8 +124,8 @@ impl PluginContext { } /// Retrieve plugin data - pub fn get_data(&self, key: &str) -> Option - where + pub fn get_data(&self, key: &str) -> Option + where T: Clone, { let store = self.data_store.read().ok()?; @@ -265,10 +265,10 @@ impl PluginManager { /// Register a plugin pub fn register_plugin(&mut self, plugin: Box) -> Result<()> { let name = plugin.metadata().name.clone(); - + // Check for duplicate names if self.plugins.contains_key(&name) { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' is already registered", name), context: None, }); @@ -277,7 +277,8 @@ impl PluginManager { // Validate dependencies self.validate_dependencies(plugin.metadata())?; - self.plugin_states.insert(name.clone(), PluginState::Unloaded); + self.plugin_states + .insert(name.clone(), PluginState::Unloaded); self.plugins.insert(name.clone(), plugin); self.load_order.push(name); @@ -287,7 +288,7 @@ impl PluginManager { /// Load a plugin pub fn load_plugin(&mut self, name: &str) -> Result<()> { if !self.plugins.contains_key(name) { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' not found", name), context: None, }); @@ -300,7 +301,7 @@ impl PluginManager { return Ok(()); // Already loaded } PluginState::Loading => { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' is already loading", name), context: None, }); @@ -310,7 +311,8 @@ impl PluginManager { } // Set loading state - self.plugin_states.insert(name.to_string(), PluginState::Loading); + self.plugin_states + .insert(name.to_string(), PluginState::Loading); // Load dependencies first let dependencies = self.plugins[name].metadata().dependencies.clone(); @@ -320,24 +322,24 @@ impl PluginManager { // Initialize plugin match self.plugins.get_mut(name) { - Some(plugin) => { - match plugin.initialize(&mut self.context) { - Ok(()) => { - self.plugin_states.insert(name.to_string(), PluginState::Loaded); - tracing::info!("Plugin '{}' loaded successfully", name); - } - Err(e) => { - let error_msg = format!("Failed to initialize plugin '{}': {}", name, e); - self.plugin_states.insert(name.to_string(), PluginState::Error(error_msg.clone())); - return Err(StratoError::PluginError { - message: error_msg, - context: None, - }); - } + Some(plugin) => match plugin.initialize(&mut self.context) { + Ok(()) => { + self.plugin_states + .insert(name.to_string(), PluginState::Loaded); + tracing::info!("Plugin '{}' loaded successfully", name); } - } + Err(e) => { + let error_msg = format!("Failed to initialize plugin '{}': {}", name, e); + self.plugin_states + .insert(name.to_string(), PluginState::Error(error_msg.clone())); + return Err(StratoError::PluginError { + message: error_msg, + context: None, + }); + } + }, None => { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' not found", name), context: None, }); @@ -359,24 +361,24 @@ impl PluginManager { // Activate plugin match self.plugins.get_mut(name) { - Some(plugin) => { - match plugin.activate(&mut self.context) { - Ok(()) => { - self.plugin_states.insert(name.to_string(), PluginState::Active); - tracing::info!("Plugin '{}' activated successfully", name); - } - Err(e) => { - let error_msg = format!("Failed to activate plugin '{}': {}", name, e); - self.plugin_states.insert(name.to_string(), PluginState::Error(error_msg.clone())); - return Err(StratoError::PluginError { - message: error_msg, - context: None, - }); - } + Some(plugin) => match plugin.activate(&mut self.context) { + Ok(()) => { + self.plugin_states + .insert(name.to_string(), PluginState::Active); + tracing::info!("Plugin '{}' activated successfully", name); } - } + Err(e) => { + let error_msg = format!("Failed to activate plugin '{}': {}", name, e); + self.plugin_states + .insert(name.to_string(), PluginState::Error(error_msg.clone())); + return Err(StratoError::PluginError { + message: error_msg, + context: None, + }); + } + }, None => { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' not found", name), context: None, }); @@ -392,11 +394,12 @@ impl PluginManager { match self.plugins.get_mut(name) { Some(plugin) => { plugin.deactivate(&mut self.context)?; - self.plugin_states.insert(name.to_string(), PluginState::Loaded); + self.plugin_states + .insert(name.to_string(), PluginState::Loaded); tracing::info!("Plugin '{}' deactivated", name); } None => { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' not found", name), context: None, }); @@ -416,11 +419,12 @@ impl PluginManager { match self.plugins.get_mut(name) { Some(plugin) => { plugin.cleanup(&mut self.context)?; - self.plugin_states.insert(name.to_string(), PluginState::Unloaded); + self.plugin_states + .insert(name.to_string(), PluginState::Unloaded); tracing::info!("Plugin '{}' unloaded", name); } None => { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!("Plugin '{}' not found", name), context: None, }); @@ -433,7 +437,7 @@ impl PluginManager { /// Load all registered plugins pub fn load_all_plugins(&mut self) -> Result<()> { let plugin_names: Vec = self.load_order.clone(); - + for name in plugin_names { if let Err(e) = self.load_plugin(&name) { tracing::error!("Failed to load plugin '{}': {}", name, e); @@ -447,7 +451,7 @@ impl PluginManager { /// Activate all loaded plugins pub fn activate_all_plugins(&mut self) -> Result<()> { let plugin_names: Vec = self.load_order.clone(); - + for name in plugin_names { if let Some(PluginState::Loaded) = self.plugin_states.get(&name) { if let Err(e) = self.activate_plugin(&name) { @@ -488,7 +492,7 @@ impl PluginManager { /// Handle event through all active plugins pub fn handle_event(&mut self, event: &Event) -> EventResult { let plugin_names: Vec = self.plugins.keys().cloned().collect(); - + for name in plugin_names { if let Some(PluginState::Active) = self.plugin_states.get(&name) { if let Some(plugin) = self.plugins.get_mut(&name) { @@ -507,7 +511,7 @@ impl PluginManager { fn validate_dependencies(&self, metadata: &PluginMetadata) -> Result<()> { for dep in &metadata.dependencies { if !self.plugins.contains_key(dep) { - return Err(StratoError::PluginError { + return Err(StratoError::PluginError { message: format!( "Plugin '{}' depends on '{}' which is not registered", metadata.name, dep @@ -625,7 +629,7 @@ mod tests { fn test_plugin_registration() { let mut manager = PluginManager::new(); let plugin = Box::new(TestPlugin::new("test")); - + assert!(manager.register_plugin(plugin).is_ok()); assert_eq!(manager.get_plugin_names().len(), 1); assert!(manager.get_plugin_names().contains(&"test".to_string())); @@ -635,12 +639,12 @@ mod tests { fn test_plugin_loading() { let mut manager = PluginManager::new(); let plugin = Box::new(TestPlugin::new("test")); - + manager.register_plugin(plugin).unwrap(); assert!(manager.load_plugin("test").is_ok()); - + match manager.get_plugin_state("test") { - Some(PluginState::Loaded) => {}, + Some(PluginState::Loaded) => {} _ => panic!("Plugin should be loaded"), } } @@ -649,12 +653,12 @@ mod tests { fn test_plugin_activation() { let mut manager = PluginManager::new(); let plugin = Box::new(TestPlugin::new("test")); - + manager.register_plugin(plugin).unwrap(); assert!(manager.activate_plugin("test").is_ok()); - + match manager.get_plugin_state("test") { - Some(PluginState::Active) => {}, + Some(PluginState::Active) => {} _ => panic!("Plugin should be active"), } } @@ -662,13 +666,13 @@ mod tests { #[test] fn test_widget_registry() { let mut registry = WidgetRegistry::new(); - + // This would normally be a real widget factory let factory: WidgetFactory = Box::new(|| { // Return a mock widget unimplemented!("Mock widget factory") }); - + registry.register("test_widget".to_string(), factory); assert!(registry.has_widget("test_widget")); assert_eq!(registry.get_widget_names().len(), 1); @@ -690,4 +694,4 @@ mod tests { assert_eq!(metadata.dependencies.len(), 2); assert_eq!(metadata.capabilities.len(), 2); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/reactive.rs b/crates/strato-core/src/reactive.rs index 116e2bc..3466dfe 100644 --- a/crates/strato-core/src/reactive.rs +++ b/crates/strato-core/src/reactive.rs @@ -1,8 +1,8 @@ //! Reactive programming primitives for StratoUI -use std::sync::Arc; use parking_lot::RwLock; use smallvec::SmallVec; +use std::sync::Arc; // Removed unused std::fmt::Debug import use std::marker::PhantomData; @@ -13,7 +13,7 @@ pub type EffectFn = Box; pub trait Reactive: Send + Sync { /// Track this reactive value as a dependency fn track(&self); - + /// Trigger updates for all dependents fn trigger(&self); } @@ -86,10 +86,10 @@ impl Effect { dependencies: Arc::new(RwLock::new(SmallVec::new())), active: Arc::new(RwLock::new(true)), }; - + // Run the effect immediately effect.run(); - + effect } @@ -234,13 +234,11 @@ mod tests { fn test_computed() { let counter = Arc::new(RwLock::new(0)); let counter_clone = counter.clone(); - - let computed = Computed::new(move || { - *counter_clone.read() * 2 - }); - + + let computed = Computed::new(move || *counter_clone.read() * 2); + assert_eq!(computed.get(), 0); - + *counter.write() = 5; computed.invalidate(); assert_eq!(computed.get(), 10); @@ -249,15 +247,15 @@ mod tests { #[test] fn test_watch() { use std::sync::atomic::{AtomicI32, Ordering}; - + let watch = Watch::new(0); let received = Arc::new(AtomicI32::new(0)); let received_clone = received.clone(); - + watch.on_change(move |value| { received_clone.store(*value, Ordering::SeqCst); }); - + watch.set(42); assert_eq!(received.load(Ordering::SeqCst), 42); } @@ -266,16 +264,16 @@ mod tests { fn test_memo() { let call_count = Arc::new(RwLock::new(0)); let call_count_clone = call_count.clone(); - + let memo = Memo::new(move || { *call_count_clone.write() += 1; 42 }); - + assert_eq!(memo.get(), 42); assert_eq!(memo.get(), 42); // Should not recompute assert_eq!(*call_count.read(), 1); - + memo.clear(); assert_eq!(memo.get(), 42); // Recomputes after clear assert_eq!(*call_count.read(), 2); diff --git a/crates/strato-core/src/state.rs b/crates/strato-core/src/state.rs index 028492b..f824c70 100644 --- a/crates/strato-core/src/state.rs +++ b/crates/strato-core/src/state.rs @@ -191,7 +191,7 @@ impl Signal { #[cfg(feature = "serde")] { // Record inspector snapshot if available. - use self::serde_helper::{JsonInspector, Fallback}; + use self::serde_helper::{Fallback, JsonInspector}; let detail = JsonInspector(&value).to_json(); crate::inspector::inspector().record_state_snapshot(self.id, detail); } @@ -214,7 +214,7 @@ impl Signal { }; #[cfg(feature = "serde")] { - use self::serde_helper::{JsonInspector, Fallback}; + use self::serde_helper::{Fallback, JsonInspector}; let detail = JsonInspector(&value).to_json(); crate::inspector::inspector().record_state_snapshot(self.id, detail); } diff --git a/crates/strato-core/src/text.rs b/crates/strato-core/src/text.rs index de4a8ec..ddb6c0d 100644 --- a/crates/strato-core/src/text.rs +++ b/crates/strato-core/src/text.rs @@ -85,16 +85,16 @@ impl Default for FontDescriptor { // Use platform-specific default fonts instead of generic "system-ui" #[cfg(target_os = "windows")] let default_family = "Segoe UI"; - + #[cfg(target_os = "macos")] let default_family = "SF Pro Display"; - + #[cfg(target_os = "linux")] let default_family = "Ubuntu"; - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let default_family = "Arial"; - + Self { family: default_family.to_string(), size: 14.0, @@ -254,12 +254,17 @@ impl FontManager { pub fn load_font_from_file(&mut self, path: &str, family: &str) -> Result<(), TextError> { let data = std::fs::read(path) .map_err(|e| TextError::FontLoadError(format!("Failed to read font file: {}", e)))?; - + self.load_font_from_data(data, family, 0) } /// Load a font from raw data - pub fn load_font_from_data(&mut self, data: Vec, family: &str, index: u32) -> Result<(), TextError> { + pub fn load_font_from_data( + &mut self, + data: Vec, + family: &str, + index: u32, + ) -> Result<(), TextError> { let font_data = FontData { family: family.to_string(), data, @@ -295,12 +300,15 @@ impl TextShaper { } /// Shape text into positioned glyphs - pub fn shape_text(&self, text: &str, style: &TextStyle) -> Result, TextError> { - + pub fn shape_text( + &self, + text: &str, + style: &TextStyle, + ) -> Result, TextError> { // TODO: Use self.font_manager to get proper font metrics let mut glyphs = Vec::new(); let mut x = 0.0; - + for (_i, ch) in text.char_indices() { let glyph = PositionedGlyph { glyph_id: ch as u32, // Simplified glyph ID @@ -310,11 +318,11 @@ impl TextShaper { advance_y: 0.0, font_size: style.font.size, }; - + x += glyph.advance_x + style.letter_spacing; glyphs.push(glyph); } - + Ok(glyphs) } } @@ -332,40 +340,58 @@ impl TextLayoutEngine { } /// Layout text according to the given configuration - pub fn layout_text(&self, text: &str, style: &TextStyle, layout: &TextLayout) -> Result { + pub fn layout_text( + &self, + text: &str, + style: &TextStyle, + layout: &TextLayout, + ) -> Result { let glyphs = self.shaper.shape_text(text, style)?; - + // Simple line breaking and layout let mut lines = Vec::new(); let mut current_line_glyphs = Vec::new(); let mut current_x = 0.0; let line_height = style.font.size * layout.line_height; - + for glyph in glyphs { if let Some(max_width) = layout.max_width { if current_x + glyph.advance_x > max_width && !current_line_glyphs.is_empty() { // Create line from current glyphs - let line = self.create_text_line(current_line_glyphs, style, current_x, line_height * lines.len() as f32); + let line = self.create_text_line( + current_line_glyphs, + style, + current_x, + line_height * lines.len() as f32, + ); lines.push(line); current_line_glyphs = Vec::new(); current_x = 0.0; } } - + current_line_glyphs.push(glyph.clone()); current_x += glyph.advance_x; } - + // Add remaining glyphs as final line if !current_line_glyphs.is_empty() { - let line = self.create_text_line(current_line_glyphs, style, current_x, line_height * lines.len() as f32); + let line = self.create_text_line( + current_line_glyphs, + style, + current_x, + line_height * lines.len() as f32, + ); lines.push(line); } - + // Calculate overall bounds - let total_width = lines.iter().map(|line| line.bounds.width).fold(0.0, f32::max); + let total_width = lines + .iter() + .map(|line| line.bounds.width) + .fold(0.0, f32::max); let total_height = lines.len() as f32 * line_height; - + Ok(LayoutResult { lines: lines.clone(), bounds: TextBounds { @@ -378,7 +404,13 @@ impl TextLayoutEngine { }) } - fn create_text_line(&self, glyphs: Vec, style: &TextStyle, width: f32, y: f32) -> TextLine { + fn create_text_line( + &self, + glyphs: Vec, + style: &TextStyle, + width: f32, + y: f32, + ) -> TextLine { let run = TextRun { text: String::new(), // Would be populated in real implementation style: style.clone(), @@ -390,7 +422,7 @@ impl TextLayoutEngine { height: style.font.size, }, }; - + TextLine { runs: vec![run], bounds: TextBounds { @@ -417,7 +449,12 @@ impl TextMeasurer { } /// Measure text dimensions - pub fn measure_text(&self, text: &str, style: &TextStyle, layout: &TextLayout) -> Result { + pub fn measure_text( + &self, + text: &str, + style: &TextStyle, + layout: &TextLayout, + ) -> Result { let result = self.layout_engine.layout_text(text, style, layout)?; Ok(result.bounds) } @@ -433,19 +470,19 @@ impl TextMeasurer { pub enum TextError { #[error("Font loading error: {0}")] FontLoadError(String), - + #[error("Text shaping error: {0}")] ShapingError(String), - + #[error("Layout error: {0}")] LayoutError(String), - + #[error("Rendering error: {0}")] RenderingError(String), - + #[error("Invalid font data")] InvalidFontData, - + #[error("Unsupported text feature: {0}")] UnsupportedFeature(String), } @@ -466,7 +503,7 @@ impl TextSystem { let font_manager = Arc::new(FontManager::new()); let layout_engine = TextLayoutEngine::new(font_manager.clone()); let measurer = TextMeasurer::new(font_manager.clone()); - + Self { font_manager, layout_engine, @@ -505,16 +542,16 @@ mod tests { #[test] fn test_font_descriptor_default() { let font = FontDescriptor::default(); - + #[cfg(target_os = "windows")] assert_eq!(font.family, "Segoe UI"); - + #[cfg(target_os = "macos")] assert_eq!(font.family, "SF Pro Display"); - + #[cfg(target_os = "linux")] assert_eq!(font.family, "Ubuntu"); - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] assert_eq!(font.family, "Arial"); @@ -533,7 +570,7 @@ mod tests { fn test_font_manager() { let mut manager = FontManager::new(); assert!(manager.get_font("nonexistent").is_none()); - + manager.add_fallback_font("Test Font".to_string()); assert!(manager.fallback_fonts.contains(&"Test Font".to_string())); } @@ -542,4 +579,4 @@ mod tests { fn test_text_system_init() { assert!(init_text_system().is_ok()); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/theme.rs b/crates/strato-core/src/theme.rs index 8e62312..c6e9bc0 100644 --- a/crates/strato-core/src/theme.rs +++ b/crates/strato-core/src/theme.rs @@ -3,10 +3,10 @@ //! Provides comprehensive theming support including dark/light modes, //! custom color schemes, typography, spacing, and component styling +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; -use parking_lot::RwLock; -use serde::{Serialize, Deserialize}; /// Color representation with alpha channel #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] @@ -31,20 +31,29 @@ impl Color { /// Create a color from hex string (#RRGGBB or #RRGGBBAA) pub fn from_hex(hex: &str) -> Result { let hex = hex.trim_start_matches('#'); - + match hex.len() { 6 => { let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid hex color")?; let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid hex color")?; let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid hex color")?; - Ok(Self::rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) + Ok(Self::rgb( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + )) } 8 => { let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid hex color")?; let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid hex color")?; let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid hex color")?; let a = u8::from_str_radix(&hex[6..8], 16).map_err(|_| "Invalid hex color")?; - Ok(Self::rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)) + Ok(Self::rgba( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + )) } _ => Err("Invalid hex color length"), } @@ -161,16 +170,16 @@ impl Default for Typography { // Use platform-specific default fonts with proper fallbacks #[cfg(target_os = "windows")] let font_family = "Segoe UI, Tahoma, Arial, sans-serif"; - + #[cfg(target_os = "macos")] let font_family = "SF Pro Display, Helvetica Neue, Arial, sans-serif"; - + #[cfg(target_os = "linux")] let font_family = "Ubuntu, DejaVu Sans, Liberation Sans, Arial, sans-serif"; - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let font_family = "Arial, sans-serif"; - + Self { font_family: font_family.to_string(), base_size: 14.0, @@ -302,22 +311,22 @@ pub struct ColorPalette { // Primary colors pub primary: Color, pub primary_variant: Color, - + // Secondary colors pub secondary: Color, pub secondary_variant: Color, - + // Background colors pub background: Color, pub surface: Color, pub surface_variant: Color, - + // Text colors pub on_primary: Color, pub on_secondary: Color, pub on_background: Color, pub on_surface: Color, - + // State colors pub error: Color, pub on_error: Color, @@ -327,12 +336,12 @@ pub struct ColorPalette { pub on_success: Color, pub info: Color, pub on_info: Color, - + // Outline and divider pub outline: Color, pub outline_variant: Color, pub divider: Color, - + // Disabled state pub disabled: Color, pub on_disabled: Color, @@ -496,7 +505,7 @@ impl ThemeManager { let mut themes = HashMap::new(); let light_theme = Theme::light(); let dark_theme = Theme::dark(); - + themes.insert(light_theme.name.clone(), light_theme.clone()); themes.insert(dark_theme.name.clone(), dark_theme.clone()); @@ -516,22 +525,20 @@ impl ThemeManager { /// Set the current theme by name pub fn set_theme(&self, theme_name: &str) -> Result<(), &'static str> { let themes = self.themes.read(); - let new_theme = themes.get(theme_name) - .ok_or("Theme not found")? - .clone(); - + let new_theme = themes.get(theme_name).ok_or("Theme not found")?.clone(); + let old_theme_name = self.current_theme.read().name.clone(); let mode_changed = self.current_theme.read().mode != new_theme.mode; - + *self.current_theme.write() = new_theme; - + // Notify listeners let event = ThemeChangeEvent { old_theme: old_theme_name, new_theme: theme_name.to_string(), mode_changed, }; - + self.notify_listeners(&event); Ok(()) } @@ -574,9 +581,8 @@ impl ThemeManager { } let mut themes = self.themes.write(); - themes.remove(theme_name) - .ok_or("Theme not found")?; - + themes.remove(theme_name).ok_or("Theme not found")?; + Ok(()) } @@ -598,7 +604,7 @@ impl ThemeManager { /// Update system theme mode (for system theme detection) pub fn update_system_theme_mode(&self, mode: ThemeMode) { *self.system_theme_mode.write() = mode; - + // If current theme is system, update accordingly if self.current_theme.read().mode == ThemeMode::System { let theme_name = match mode { @@ -621,9 +627,8 @@ impl ThemeManager { /// Export theme to JSON pub fn export_theme(&self, theme_name: &str) -> Result> { let themes = self.themes.read(); - let theme = themes.get(theme_name) - .ok_or("Theme not found")?; - + let theme = themes.get(theme_name).ok_or("Theme not found")?; + Ok(serde_json::to_string_pretty(theme)?) } @@ -664,19 +669,31 @@ pub mod utils { pub fn contrast_ratio(color1: &Color, color2: &Color) -> f32 { let l1 = relative_luminance(color1); let l2 = relative_luminance(color2); - + let lighter = l1.max(l2); let darker = l1.min(l2); - + (lighter + 0.05) / (darker + 0.05) } /// Calculate relative luminance of a color fn relative_luminance(color: &Color) -> f32 { - let r = if color.r <= 0.03928 { color.r / 12.92 } else { ((color.r + 0.055) / 1.055).powf(2.4) }; - let g = if color.g <= 0.03928 { color.g / 12.92 } else { ((color.g + 0.055) / 1.055).powf(2.4) }; - let b = if color.b <= 0.03928 { color.b / 12.92 } else { ((color.b + 0.055) / 1.055).powf(2.4) }; - + let r = if color.r <= 0.03928 { + color.r / 12.92 + } else { + ((color.r + 0.055) / 1.055).powf(2.4) + }; + let g = if color.g <= 0.03928 { + color.g / 12.92 + } else { + ((color.g + 0.055) / 1.055).powf(2.4) + }; + let b = if color.b <= 0.03928 { + color.b / 12.92 + } else { + ((color.b + 0.055) / 1.055).powf(2.4) + }; + 0.2126 * r + 0.7152 * g + 0.0722 * b } @@ -747,7 +764,7 @@ mod tests { fn test_theme_manager() { let manager = ThemeManager::new(); assert_eq!(manager.current_theme().name, "Light"); - + manager.set_theme("Dark").unwrap(); assert_eq!(manager.current_theme().name, "Dark"); } @@ -767,4 +784,4 @@ mod tests { assert!(utils::meets_wcag_aa(&white, &black)); assert!(utils::meets_wcag_aaa(&white, &black)); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/types.rs b/crates/strato-core/src/types.rs index 03b2bd1..31910b0 100644 --- a/crates/strato-core/src/types.rs +++ b/crates/strato-core/src/types.rs @@ -107,7 +107,7 @@ impl Color { if hex.len() != 6 && hex.len() != 8 { return Err(format!("Invalid hex color: {}", hex)); } - + let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?; let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?; let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?; @@ -116,7 +116,7 @@ impl Color { } else { 255 }; - + Ok(Self { r: r as f32 / 255.0, g: g as f32 / 255.0, @@ -127,7 +127,8 @@ impl Color { /// Convert to hex string pub fn to_hex(&self) -> String { - format!("#{:02x}{:02x}{:02x}{:02x}", + format!( + "#{:02x}{:02x}{:02x}{:02x}", (self.r * 255.0).round() as u8, (self.g * 255.0).round() as u8, (self.b * 255.0).round() as u8, @@ -161,22 +162,92 @@ impl Color { } /// Common colors - pub const WHITE: Self = Self { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; - pub const BLACK: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }; - pub const RED: Self = Self { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }; - pub const GREEN: Self = Self { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }; - pub const BLUE: Self = Self { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }; - pub const TRANSPARENT: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }; - pub const GRAY: Self = Self { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }; - pub const LIGHT_GRAY: Self = Self { r: 0.8, g: 0.8, b: 0.8, a: 1.0 }; - pub const DARK_GRAY: Self = Self { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }; - + pub const WHITE: Self = Self { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, + }; + pub const BLACK: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + pub const RED: Self = Self { + r: 1.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + pub const GREEN: Self = Self { + r: 0.0, + g: 1.0, + b: 0.0, + a: 1.0, + }; + pub const BLUE: Self = Self { + r: 0.0, + g: 0.0, + b: 1.0, + a: 1.0, + }; + pub const TRANSPARENT: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + pub const GRAY: Self = Self { + r: 0.5, + g: 0.5, + b: 0.5, + a: 1.0, + }; + pub const LIGHT_GRAY: Self = Self { + r: 0.8, + g: 0.8, + b: 0.8, + a: 1.0, + }; + pub const DARK_GRAY: Self = Self { + r: 0.3, + g: 0.3, + b: 0.3, + a: 1.0, + }; + // Material Design colors - pub const PRIMARY: Self = Self { r: 0.129, g: 0.588, b: 0.953, a: 1.0 }; // Blue 500 - pub const SECONDARY: Self = Self { r: 0.0, g: 0.737, b: 0.831, a: 1.0 }; // Cyan 500 - pub const SUCCESS: Self = Self { r: 0.298, g: 0.686, b: 0.314, a: 1.0 }; // Green 500 - pub const WARNING: Self = Self { r: 1.0, g: 0.757, b: 0.027, a: 1.0 }; // Amber 500 - pub const ERROR: Self = Self { r: 0.956, g: 0.263, b: 0.212, a: 1.0 }; // Red 500 + pub const PRIMARY: Self = Self { + r: 0.129, + g: 0.588, + b: 0.953, + a: 1.0, + }; // Blue 500 + pub const SECONDARY: Self = Self { + r: 0.0, + g: 0.737, + b: 0.831, + a: 1.0, + }; // Cyan 500 + pub const SUCCESS: Self = Self { + r: 0.298, + g: 0.686, + b: 0.314, + a: 1.0, + }; // Green 500 + pub const WARNING: Self = Self { + r: 1.0, + g: 0.757, + b: 0.027, + a: 1.0, + }; // Amber 500 + pub const ERROR: Self = Self { + r: 0.956, + g: 0.263, + b: 0.212, + a: 1.0, + }; // Red 500 } impl Default for Color { @@ -232,7 +303,12 @@ pub struct Rect { impl Rect { /// Create a new rectangle pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { - Self { x, y, width, height } + Self { + x, + y, + width, + height, + } } /// Create from position and size @@ -252,16 +328,18 @@ impl Rect { /// Check if a point is inside the rectangle pub fn contains(&self, point: Point) -> bool { - point.x >= self.x && point.x <= self.x + self.width && - point.y >= self.y && point.y <= self.y + self.height + point.x >= self.x + && point.x <= self.x + self.width + && point.y >= self.y + && point.y <= self.y + self.height } /// Check if this rectangle intersects with another pub fn intersects(&self, other: &Rect) -> bool { - self.x < other.x + other.width && - self.x + self.width > other.x && - self.y < other.y + other.height && - self.y + self.height > other.y + self.x < other.x + other.width + && self.x + self.width > other.x + && self.y < other.y + other.height + && self.y + self.height > other.y } /// Get the intersection of two rectangles diff --git a/crates/strato-core/src/ui_node.rs b/crates/strato-core/src/ui_node.rs index adf9504..5c9e07f 100644 --- a/crates/strato-core/src/ui_node.rs +++ b/crates/strato-core/src/ui_node.rs @@ -1,4 +1,3 @@ - use crate::types::Color; /// A node in the semantic UI tree. @@ -90,26 +89,42 @@ impl WidgetNode { // Initial `From` implementations for easy conversion in macro generation impl From for PropValue { - fn from(v: String) -> Self { PropValue::String(v) } + fn from(v: String) -> Self { + PropValue::String(v) + } } impl From<&str> for PropValue { - fn from(v: &str) -> Self { PropValue::String(v.to_string()) } + fn from(v: &str) -> Self { + PropValue::String(v.to_string()) + } } impl From for PropValue { - fn from(v: i64) -> Self { PropValue::Int(v) } + fn from(v: i64) -> Self { + PropValue::Int(v) + } } impl From for PropValue { - fn from(v: i32) -> Self { PropValue::Int(v as i64) } + fn from(v: i32) -> Self { + PropValue::Int(v as i64) + } } impl From for PropValue { - fn from(v: f64) -> Self { PropValue::Float(v) } + fn from(v: f64) -> Self { + PropValue::Float(v) + } } impl From for PropValue { - fn from(v: f32) -> Self { PropValue::Float(v as f64) } + fn from(v: f32) -> Self { + PropValue::Float(v as f64) + } } impl From for PropValue { - fn from(v: bool) -> Self { PropValue::Bool(v) } + fn from(v: bool) -> Self { + PropValue::Bool(v) + } } impl From for PropValue { - fn from(v: Color) -> Self { PropValue::Color(v) } + fn from(v: Color) -> Self { + PropValue::Color(v) + } } diff --git a/crates/strato-core/src/vdom.rs b/crates/strato-core/src/vdom.rs index de83b72..d47f3ed 100644 --- a/crates/strato-core/src/vdom.rs +++ b/crates/strato-core/src/vdom.rs @@ -184,7 +184,12 @@ impl VNode { impl fmt::Display for VNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - VNode::Element { tag, attributes, children, .. } => { + VNode::Element { + tag, + attributes, + children, + .. + } => { write!(f, "<{}", tag)?; for (key, value) in attributes { write!(f, " {}=\"{}\"", key, value)?; @@ -200,7 +205,12 @@ impl fmt::Display for VNode { } } VNode::Text(content) => write!(f, "{}", content), - VNode::Component { name, props, children, .. } => { + VNode::Component { + name, + props, + children, + .. + } => { write!(f, "<{}", name)?; for (key, value) in props { write!(f, " {}=\"{}\"", key, value)?; @@ -261,7 +271,10 @@ impl VDomDiffer { pub fn diff(&mut self, new_tree: VNode) -> Vec { let ops = match &self.current { Some(old_tree) => self.diff_nodes(old_tree, &new_tree, 0), - None => vec![DiffOp::Insert { index: 0, node: new_tree.clone() }], + None => vec![DiffOp::Insert { + index: 0, + node: new_tree.clone(), + }], }; self.current = Some(new_tree); @@ -285,8 +298,18 @@ impl VDomDiffer { // Both are elements with the same tag ( - VNode::Element { tag: old_tag, attributes: old_attrs, children: old_children, .. }, - VNode::Element { tag: new_tag, attributes: new_attrs, children: new_children, .. }, + VNode::Element { + tag: old_tag, + attributes: old_attrs, + children: old_children, + .. + }, + VNode::Element { + tag: new_tag, + attributes: new_attrs, + children: new_children, + .. + }, ) if old_tag == new_tag => { // Diff attributes let attr_diff = self.diff_attributes(old_attrs, new_attrs); @@ -303,8 +326,18 @@ impl VDomDiffer { // Both are components with the same name ( - VNode::Component { name: old_name, props: old_props, children: old_children, .. }, - VNode::Component { name: new_name, props: new_props, children: new_children, .. }, + VNode::Component { + name: old_name, + props: old_props, + children: old_children, + .. + }, + VNode::Component { + name: new_name, + props: new_props, + children: new_children, + .. + }, ) if old_name == new_name => { // Diff props (treated like attributes) let prop_diff = self.diff_attributes(old_props, new_props); @@ -368,7 +401,12 @@ impl VDomDiffer { } /// Diff children using a keyed diffing algorithm - fn diff_children(&self, old_children: &[VNode], new_children: &[VNode], parent_index: usize) -> Vec { + fn diff_children( + &self, + old_children: &[VNode], + new_children: &[VNode], + parent_index: usize, + ) -> Vec { let mut ops = Vec::new(); // Simple algorithm for now - can be optimized with keyed diffing later @@ -486,13 +524,13 @@ mod tests { #[test] fn test_vdom_diff_text_change() { let mut differ = VDomDiffer::new(); - + let old_tree = VNode::text("Hello"); let new_tree = VNode::text("World"); - + differ.current = Some(old_tree); let ops = differ.diff(new_tree); - + assert_eq!(ops.len(), 1); match &ops[0] { DiffOp::UpdateText { text, .. } => assert_eq!(text, "World"), @@ -503,13 +541,13 @@ mod tests { #[test] fn test_vdom_diff_attribute_change() { let mut differ = VDomDiffer::new(); - + let old_tree = VNode::element("div").attr("class", "old"); let new_tree = VNode::element("div").attr("class", "new"); - + differ.current = Some(old_tree); let ops = differ.diff(new_tree); - + assert_eq!(ops.len(), 1); match &ops[0] { DiffOp::UpdateAttributes { attributes, .. } => { @@ -522,13 +560,12 @@ mod tests { #[test] fn test_vdom_tree_update() { let mut tree = VDomTree::new(); - - let root = VNode::element("div") - .child(VNode::text("Hello")); - + + let root = VNode::element("div").child(VNode::text("Hello")); + let ops = tree.update(root); assert_eq!(ops.len(), 1); - + match &ops[0] { DiffOp::Insert { node, .. } => { assert_eq!(node.get_tag(), Some("div")); @@ -536,4 +573,4 @@ mod tests { _ => panic!("Expected Insert operation"), } } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/widget.rs b/crates/strato-core/src/widget.rs index 5645c32..3279d93 100644 --- a/crates/strato-core/src/widget.rs +++ b/crates/strato-core/src/widget.rs @@ -5,16 +5,12 @@ //! creation, rendering, and interaction. use crate::{ + error::StratoResult, event::Event, layout::{LayoutConstraints, Size}, types::Rect, - error::StratoResult, -}; -use std::{ - any::Any, - collections::HashMap, - fmt::Debug, }; +use std::{any::Any, collections::HashMap, fmt::Debug}; /// Unique identifier for widgets #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -88,7 +84,11 @@ pub trait Widget: Debug + Send + Sync { fn update(&mut self, context: &mut WidgetContext) -> StratoResult<()>; /// Calculate the widget's layout - fn layout(&mut self, constraints: &LayoutConstraints, context: &mut WidgetContext) -> StratoResult; + fn layout( + &mut self, + constraints: &LayoutConstraints, + context: &mut WidgetContext, + ) -> StratoResult; /// Render the widget fn render(&self, context: &WidgetContext) -> StratoResult<()>; @@ -150,7 +150,11 @@ impl Widget for Box { (**self).update(context) } - fn layout(&mut self, constraints: &LayoutConstraints, context: &mut WidgetContext) -> StratoResult { + fn layout( + &mut self, + constraints: &LayoutConstraints, + context: &mut WidgetContext, + ) -> StratoResult { (**self).layout(constraints, context) } @@ -298,10 +302,9 @@ impl WidgetManager { pub fn handle_event(&mut self, event: &Event) -> StratoResult { if let Some(root) = self.tree.root() { let id = root.id(); - if let (Some(widget), Some(context)) = ( - self.tree.get_widget_mut(id), - self.contexts.get_mut(&id) - ) { + if let (Some(widget), Some(context)) = + (self.tree.get_widget_mut(id), self.contexts.get_mut(&id)) + { return widget.handle_event(event, context); } } @@ -311,10 +314,9 @@ impl WidgetManager { /// Update all widgets pub fn update(&mut self) -> StratoResult<()> { for id in self.tree.widget_ids() { - if let (Some(widget), Some(context)) = ( - self.tree.get_widget_mut(id), - self.contexts.get_mut(&id) - ) { + if let (Some(widget), Some(context)) = + (self.tree.get_widget_mut(id), self.contexts.get_mut(&id)) + { widget.update(context)?; } } @@ -325,10 +327,9 @@ impl WidgetManager { pub fn layout(&mut self, constraints: &LayoutConstraints) -> StratoResult<()> { if let Some(root) = self.tree.root() { let id = root.id(); - if let (Some(widget), Some(context)) = ( - self.tree.get_widget_mut(id), - self.contexts.get_mut(&id) - ) { + if let (Some(widget), Some(context)) = + (self.tree.get_widget_mut(id), self.contexts.get_mut(&id)) + { widget.layout(constraints, context)?; } } @@ -338,10 +339,9 @@ impl WidgetManager { /// Render all widgets pub fn render(&self) -> StratoResult<()> { for id in self.tree.widget_ids() { - if let (Some(widget), Some(context)) = ( - self.tree.get_widget(id), - self.contexts.get(&id) - ) { + if let (Some(widget), Some(context)) = + (self.tree.get_widget(id), self.contexts.get(&id)) + { widget.render(context)?; } } @@ -412,7 +412,11 @@ mod tests { self.id } - fn handle_event(&mut self, _event: &Event, _context: &mut WidgetContext) -> StratoResult { + fn handle_event( + &mut self, + _event: &Event, + _context: &mut WidgetContext, + ) -> StratoResult { Ok(false) } @@ -420,7 +424,11 @@ mod tests { Ok(()) } - fn layout(&mut self, _constraints: &LayoutConstraints, _context: &mut WidgetContext) -> StratoResult { + fn layout( + &mut self, + _constraints: &LayoutConstraints, + _context: &mut WidgetContext, + ) -> StratoResult { Ok(Size::new(100.0, 50.0)) } @@ -448,7 +456,7 @@ mod tests { fn test_widget_context() { let id = WidgetId::new(); let mut context = WidgetContext::new(id); - + context.set_property("test", 42i32); assert_eq!(context.get_property::("test"), Some(&42)); } @@ -458,10 +466,10 @@ mod tests { let mut tree = WidgetTree::new(); let widget = Box::new(TestWidget::new()); let id = widget.id(); - + tree.add_widget(widget); assert!(tree.get_widget(id).is_some()); - + tree.remove_widget(id); assert!(tree.get_widget(id).is_none()); } @@ -471,11 +479,11 @@ mod tests { let mut manager = WidgetManager::new(); let widget = Box::new(TestWidget::new()); let id = widget.id(); - + manager.set_root(widget); assert!(manager.get_context(id).is_some()); - + manager.set_focus(Some(id)).unwrap(); assert_eq!(manager.focused_widget(), Some(id)); } -} \ No newline at end of file +} diff --git a/crates/strato-core/src/window.rs b/crates/strato-core/src/window.rs index aac7e7d..f92ae2b 100644 --- a/crates/strato-core/src/window.rs +++ b/crates/strato-core/src/window.rs @@ -4,9 +4,9 @@ //! capabilities. It handles window lifecycle, events, and properties. use crate::{ + error::{StratoError, StratoResult}, event::Event, - types::{Size, Point, Rect}, - error::{StratoResult, StratoError}, + types::{Point, Rect, Size}, }; use std::collections::HashMap; @@ -141,22 +141,22 @@ impl Default for WindowProperties { pub trait WindowEventHandler: Send + Sync { /// Handle window close request fn on_close_requested(&mut self, window_id: WindowId) -> bool; - + /// Handle window resize fn on_resize(&mut self, window_id: WindowId, size: Size); - + /// Handle window move fn on_move(&mut self, window_id: WindowId, position: Point); - + /// Handle window focus change fn on_focus_changed(&mut self, window_id: WindowId, focused: bool); - + /// Handle window state change fn on_state_changed(&mut self, window_id: WindowId, state: WindowState); - + /// Handle window theme change fn on_theme_changed(&mut self, window_id: WindowId, theme: WindowTheme); - + /// Handle generic window event fn on_event(&mut self, window_id: WindowId, event: &Event); } @@ -169,17 +169,17 @@ impl WindowEventHandler for DefaultWindowEventHandler { fn on_close_requested(&mut self, _window_id: WindowId) -> bool { true // Allow close by default } - + fn on_resize(&mut self, _window_id: WindowId, _size: Size) {} - + fn on_move(&mut self, _window_id: WindowId, _position: Point) {} - + fn on_focus_changed(&mut self, _window_id: WindowId, _focused: bool) {} - + fn on_state_changed(&mut self, _window_id: WindowId, _state: WindowState) {} - + fn on_theme_changed(&mut self, _window_id: WindowId, _theme: WindowTheme) {} - + fn on_event(&mut self, _window_id: WindowId, _event: &Event) {} } @@ -187,52 +187,52 @@ impl WindowEventHandler for DefaultWindowEventHandler { pub trait Window: Send + Sync { /// Get the window ID fn id(&self) -> WindowId; - + /// Get window properties fn properties(&self) -> &WindowProperties; - + /// Set window title fn set_title(&mut self, title: &str) -> StratoResult<()>; - + /// Set window size fn set_size(&mut self, size: Size) -> StratoResult<()>; - + /// Set window position fn set_position(&mut self, position: Point) -> StratoResult<()>; - + /// Set window state fn set_state(&mut self, state: WindowState) -> StratoResult<()>; - + /// Set window visibility fn set_visible(&mut self, visible: bool) -> StratoResult<()>; - + /// Focus the window fn focus(&mut self) -> StratoResult<()>; - + /// Close the window fn close(&mut self) -> StratoResult<()>; - + /// Check if the window should close fn should_close(&self) -> bool; - + /// Get the window's content area fn content_area(&self) -> Rect; - + /// Get the window's scale factor fn scale_factor(&self) -> f32; - + /// Request a redraw fn request_redraw(&mut self); - + /// Set the window theme fn set_theme(&mut self, theme: WindowTheme) -> StratoResult<()>; - + /// Get the current cursor position relative to the window fn cursor_position(&self) -> Option; - + /// Set the cursor icon fn set_cursor_icon(&mut self, icon: CursorIcon) -> StratoResult<()>; - + /// Set the cursor visibility fn set_cursor_visible(&mut self, visible: bool) -> StratoResult<()>; } @@ -282,83 +282,83 @@ impl WindowBuilder { event_handler: None, } } - + /// Set the window title pub fn title>(mut self, title: S) -> Self { self.config.title = title.into(); self } - + /// Set the window size pub fn size(mut self, size: Size) -> Self { self.config.size = size; self } - + /// Set the window position pub fn position(mut self, position: Point) -> Self { self.config.position = Some(position); self } - + /// Set whether the window is resizable pub fn resizable(mut self, resizable: bool) -> Self { self.config.resizable = resizable; self } - + /// Set whether the window has decorations pub fn decorated(mut self, decorated: bool) -> Self { self.config.decorated = decorated; self } - + /// Set whether the window is always on top pub fn always_on_top(mut self, always_on_top: bool) -> Self { self.config.always_on_top = always_on_top; self } - + /// Set whether the window starts maximized pub fn maximized(mut self, maximized: bool) -> Self { self.config.maximized = maximized; self } - + /// Set whether the window starts visible pub fn visible(mut self, visible: bool) -> Self { self.config.visible = visible; self } - + /// Set whether the window is transparent pub fn transparent(mut self, transparent: bool) -> Self { self.config.transparent = transparent; self } - + /// Set the minimum window size pub fn min_size(mut self, min_size: Size) -> Self { self.config.min_size = Some(min_size); self } - + /// Set the maximum window size pub fn max_size(mut self, max_size: Size) -> Self { self.config.max_size = Some(max_size); self } - + /// Set the event handler pub fn event_handler(mut self, handler: H) -> Self { self.event_handler = Some(Box::new(handler)); self } - + /// Build the window pub fn build(self) -> StratoResult> { // This would be implemented by the platform-specific backend - Err(StratoError::NotImplemented { + Err(StratoError::NotImplemented { message: "Window creation not implemented".to_string(), context: None, }) @@ -387,48 +387,48 @@ impl WindowManager { active_window: None, } } - + /// Create a new window pub fn create_window(&mut self, builder: WindowBuilder) -> StratoResult { let window = builder.build()?; let id = window.id(); self.windows.insert(id, window); - + if self.active_window.is_none() { self.active_window = Some(id); } - + Ok(id) } - + /// Get a window by ID pub fn get_window(&self, id: WindowId) -> Option<&dyn Window> { self.windows.get(&id).map(|w| w.as_ref()) } - + /// Get a mutable window by ID pub fn get_window_mut(&mut self, id: WindowId) -> Option<&mut Box> { self.windows.get_mut(&id) } - + /// Close a window pub fn close_window(&mut self, id: WindowId) -> StratoResult<()> { if let Some(mut window) = self.windows.remove(&id) { window.close()?; self.event_handlers.remove(&id); - + if self.active_window == Some(id) { self.active_window = self.windows.keys().next().copied(); } } Ok(()) } - + /// Get the active window ID pub fn active_window(&self) -> Option { self.active_window } - + /// Set the active window pub fn set_active_window(&mut self, id: WindowId) -> StratoResult<()> { if self.windows.contains_key(&id) { @@ -439,12 +439,12 @@ impl WindowManager { } Ok(()) } - + /// Get all window IDs pub fn window_ids(&self) -> Vec { self.windows.keys().copied().collect() } - + /// Handle an event for a specific window pub fn handle_event(&mut self, window_id: WindowId, event: &Event) -> StratoResult<()> { if let Some(handler) = self.event_handlers.get_mut(&window_id) { @@ -452,7 +452,7 @@ impl WindowManager { } Ok(()) } - + /// Update all windows pub fn update(&mut self) -> StratoResult<()> { // Process window events and updates @@ -467,7 +467,7 @@ impl WindowManager { } } } - + // Remove windows that should be closed let mut to_remove = Vec::new(); for (id, window) in &self.windows { @@ -475,14 +475,14 @@ impl WindowManager { to_remove.push(*id); } } - + for id in to_remove { self.close_window(id)?; } - + Ok(()) } - + /// Check if there are any open windows pub fn has_windows(&self) -> bool { !self.windows.is_empty() @@ -523,7 +523,7 @@ mod tests { .size(Size::new(1024.0, 768.0)) .resizable(false) .decorated(true); - + assert_eq!(builder.config.title, "Test Window"); assert_eq!(builder.config.size, Size::new(1024.0, 768.0)); assert!(!builder.config.resizable); @@ -565,7 +565,7 @@ mod tests { CursorIcon::Help, CursorIcon::Progress, ]; - + // Test that all variants are distinct for (i, icon1) in icons.iter().enumerate() { for (j, icon2) in icons.iter().enumerate() { @@ -575,4 +575,4 @@ mod tests { } } } -} \ No newline at end of file +} diff --git a/crates/strato-macros/src/lib.rs b/crates/strato-macros/src/lib.rs index 337f2be..079f02c 100644 --- a/crates/strato-macros/src/lib.rs +++ b/crates/strato-macros/src/lib.rs @@ -1,11 +1,11 @@ use proc_macro::TokenStream; use quote::{quote, ToTokens}; use syn::{ + braced, bracketed, parse::{Parse, ParseStream}, - parse_macro_input, Token, Ident, Expr, Lit, + parse_macro_input, punctuated::Punctuated, - bracketed, braced, - + Expr, Ident, Lit, Token, }; // --- Parsed Structures --- @@ -56,46 +56,46 @@ impl Parse for WidgetNode { let mut children = None; if !content.is_empty() { - let is_key_value = if content.peek(Ident) { + let is_key_value = if content.peek(Ident) { content.peek2(Token![:]) - } else { + } else { false - }; + }; - if !is_key_value { + if !is_key_value { let arg: Expr = content.parse()?; builder_arg = Some(arg); - + if content.peek(Token![,]) { content.parse::()?; } - } + } } while !content.is_empty() { if content.peek(Ident) && content.peek2(Token![:]) { let key: Ident = content.parse()?; content.parse::()?; - + if key == "children" { let children_content; bracketed!(children_content in content); - - let parsed_children: Punctuated = + + let parsed_children: Punctuated = children_content.parse_terminated(Child::parse, Token![,])?; children = Some(parsed_children.into_iter().collect()); } else { // Parse value: could be WidgetNode (DSL) or Expr let value = if content.peek(Ident) && content.peek2(syn::token::Brace) { - let node: WidgetNode = content.parse()?; - PropValue::Node(node) + let node: WidgetNode = content.parse()?; + PropValue::Node(node) } else { - let expr: Expr = content.parse()?; - PropValue::Expr(expr) + let expr: Expr = content.parse()?; + PropValue::Expr(expr) }; props.push(Prop { name: key, value }); } - + if content.peek(Token![,]) { content.parse::()?; } @@ -104,15 +104,20 @@ impl Parse for WidgetNode { } } - Ok(WidgetNode { name, builder_arg, props, children }) + Ok(WidgetNode { + name, + builder_arg, + props, + children, + }) } } impl Parse for Child { fn parse(input: ParseStream) -> syn::Result { if input.peek(Ident) && input.peek2(syn::token::Brace) { - let node: WidgetNode = input.parse()?; - Ok(Child::Node(node)) + let node: WidgetNode = input.parse()?; + Ok(Child::Node(node)) } else { let expr: Expr = input.parse()?; Ok(Child::Expr(expr)) @@ -124,7 +129,7 @@ impl Parse for Child { impl ToTokens for View { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - // Return the root UiNode + // Return the root UiNode self.root.to_tokens(tokens); } } @@ -133,11 +138,13 @@ impl ToTokens for PropValue { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { match self { PropValue::Node(_) => { - tokens.extend(quote! { compile_error!("Unexpected nested widget in PropValue generation") }); - }, + tokens.extend( + quote! { compile_error!("Unexpected nested widget in PropValue generation") }, + ); + } PropValue::Expr(expr) => { - tokens.extend(quote! { strato_core::ui_node::PropValue::from(#expr) }); - }, + tokens.extend(quote! { strato_core::ui_node::PropValue::from(#expr) }); + } } } } @@ -146,52 +153,55 @@ impl ToTokens for WidgetNode { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let name_str = self.name.to_string(); let props = &self.props; - + // Handle props let mut prop_tokens = Vec::new(); - + // 1. Add constructor arg as "value" or specific prop if let Some(arg) = &self.builder_arg { - // Heuristic: Text/Button -> "text", others -> "value" - let prop_name = match name_str.as_str() { - "Text" | "Button" | "Label" => "text", - "Image" => "source", - _ => "value", - }; - - prop_tokens.push(quote! { - (#prop_name.to_string(), strato_core::ui_node::PropValue::from(#arg)) - }); + // Heuristic: Text/Button -> "text", others -> "value" + let prop_name = match name_str.as_str() { + "Text" | "Button" | "Label" => "text", + "Image" => "source", + _ => "value", + }; + + prop_tokens.push(quote! { + (#prop_name.to_string(), strato_core::ui_node::PropValue::from(#arg)) + }); } - + // 2. Add standard props for prop in props { let key = prop.name.to_string(); let value = &prop.value; - + match value { - PropValue::Node(_node) => { - if key == "child" { - // Handled in children section - } else { - // ERROR: Nested widgets in props (other than child) are FORBIDDEN in this pure AST. - // We could panic here or emit a compile error. - // For now, emit a compile error via quote if possible, or just ignore. - // panic!("Nested widgets in properties (except 'child') are not supported in Semantic AST. Found widget in '{}'", key); - - // Better: prevent compilation - let err_msg = format!("Property '{}' contains a Widget. Widgets can only be children.", key); - prop_tokens.push(quote! { compile_error!(#err_msg) }); - } - } - PropValue::Expr(expr) => { - prop_tokens.push(quote! { - (#key.to_string(), strato_core::ui_node::PropValue::from(#expr)) - }); - } + PropValue::Node(_node) => { + if key == "child" { + // Handled in children section + } else { + // ERROR: Nested widgets in props (other than child) are FORBIDDEN in this pure AST. + // We could panic here or emit a compile error. + // For now, emit a compile error via quote if possible, or just ignore. + // panic!("Nested widgets in properties (except 'child') are not supported in Semantic AST. Found widget in '{}'", key); + + // Better: prevent compilation + let err_msg = format!( + "Property '{}' contains a Widget. Widgets can only be children.", + key + ); + prop_tokens.push(quote! { compile_error!(#err_msg) }); + } + } + PropValue::Expr(expr) => { + prop_tokens.push(quote! { + (#key.to_string(), strato_core::ui_node::PropValue::from(#expr)) + }); + } } } - + let mut children_tokens = Vec::new(); // 1. Explicit children from `children: [...]` if let Some(children) = &self.children { @@ -202,18 +212,23 @@ impl ToTokens for WidgetNode { } Child::Expr(expr) => { // Heuristic: string literal -> Text node - if let Expr::Lit(syn::ExprLit { lit: Lit::Str(_), .. }) = expr { - children_tokens.push(quote! { strato_core::ui_node::UiNode::Text(#expr.to_string()) }); - } else { - // Dynamic expression? We can't easily turn it into UiNode unless it IS a UiNode. - // Assuming expression evaluates to UiNode. - children_tokens.push(quote! { #expr }); - } + if let Expr::Lit(syn::ExprLit { + lit: Lit::Str(_), .. + }) = expr + { + children_tokens.push( + quote! { strato_core::ui_node::UiNode::Text(#expr.to_string()) }, + ); + } else { + // Dynamic expression? We can't easily turn it into UiNode unless it IS a UiNode. + // Assuming expression evaluates to UiNode. + children_tokens.push(quote! { #expr }); + } } } } } - + // 2. "child" prop moved to children for prop in props { if prop.name == "child" { @@ -257,7 +272,8 @@ pub fn view(input: TokenStream) -> TokenStream { use strato_widgets::prelude::*; #view_def } - }.into() + } + .into() } /// Derive macro for Widget trait (Placeholder) diff --git a/crates/strato-platform/src/application.rs b/crates/strato-platform/src/application.rs index 2986501..c4ba0b4 100644 --- a/crates/strato-platform/src/application.rs +++ b/crates/strato-platform/src/application.rs @@ -1,9 +1,9 @@ //! Application management +use crate::{EventLoop, Window, WindowBuilder}; +use std::collections::HashMap; use strato_core::event::Event; use strato_widgets::widget::Widget; -use crate::{Window, WindowBuilder, EventLoop}; -use std::collections::HashMap; /// Application builder pub struct ApplicationBuilder { @@ -96,12 +96,11 @@ impl Application { self.windows.get_mut(&id) } - /// Render the application with a simple approach (no actual GPU rendering) pub fn render_simple(&mut self, window_width: f32, window_height: f32) -> anyhow::Result<()> { if let Some(root_widget) = self.root_widget.as_mut() { let mut batch = strato_renderer::RenderBatch::new(); - + // Compute layout constraints using actual window size let constraints = strato_core::layout::Constraints { min_width: window_width, @@ -109,20 +108,14 @@ impl Application { min_height: window_height, max_height: window_height, }; - - // Layout and render the root widget let size = root_widget.layout(constraints); - let layout = strato_core::layout::Layout::new( - glam::Vec2::new(0.0, 0.0), - size - ); + let layout = strato_core::layout::Layout::new(glam::Vec2::new(0.0, 0.0), size); root_widget.render(&mut batch, layout); - - + tracing::info!("Rendered {} vertices in batch", batch.vertices.len()); - + // Return the batch for actual rendering self.render_batch = Some(batch); } else { @@ -130,7 +123,7 @@ impl Application { } Ok(()) } - + /// Get the current render batch pub fn get_render_batch(&mut self) -> Option { self.render_batch.take() @@ -166,7 +159,7 @@ impl Application { } } } - + #[cfg(target_arch = "wasm32")] { panic!("WebAssembly platform not fully implemented"); @@ -185,7 +178,7 @@ impl Application { Event::Window(strato_core::event::WindowEvent::Close) => { // Handle window close - for now, just log it // The application will continue running - use strato_core::{strato_info, logging::LogCategory}; + use strato_core::{logging::LogCategory, strato_info}; strato_info!(LogCategory::Platform, "Window close requested"); } _ => {} diff --git a/crates/strato-platform/src/desktop.rs b/crates/strato-platform/src/desktop.rs index 52a1cb0..e505491 100644 --- a/crates/strato-platform/src/desktop.rs +++ b/crates/strato-platform/src/desktop.rs @@ -1,11 +1,11 @@ //! Desktop platform implementation -use crate::{Platform, PlatformError, Window, WindowBuilder, WindowId}; -use crate::window::WindowInner; use crate::event_loop::CustomEvent; -use strato_core::event::Event; -use std::sync::Arc; +use crate::window::WindowInner; +use crate::{Platform, PlatformError, Window, WindowBuilder, WindowId}; use std::collections::HashMap; +use std::sync::Arc; +use strato_core::event::Event; /// Desktop platform implementation pub struct DesktopPlatform { @@ -18,7 +18,11 @@ impl DesktopPlatform { /// Create a new desktop platform pub fn new() -> Self { Self { - event_loop: Some(winit::event_loop::EventLoopBuilder::with_user_event().build().expect("Failed to create event loop")), + event_loop: Some( + winit::event_loop::EventLoopBuilder::with_user_event() + .build() + .expect("Failed to create event loop"), + ), windows: HashMap::new(), next_window_id: 0, } @@ -31,10 +35,13 @@ impl Platform for DesktopPlatform { } fn create_window(&mut self, builder: WindowBuilder) -> Result { - let event_loop = self.event_loop.as_ref() + let event_loop = self + .event_loop + .as_ref() .ok_or_else(|| PlatformError::EventLoop("Event loop not available".to_string()))?; - let winit_window = builder.build_winit(event_loop) + let winit_window = builder + .build_winit(event_loop) .map_err(|e| PlatformError::WindowCreation(e.to_string()))?; let window_arc = Arc::new(winit_window); @@ -49,12 +56,17 @@ impl Platform for DesktopPlatform { }) } - fn run_event_loop(&mut self, mut callback: Box) -> Result<(), PlatformError> { - let event_loop = self.event_loop.take() + fn run_event_loop( + &mut self, + mut callback: Box, + ) -> Result<(), PlatformError> { + let event_loop = self + .event_loop + .take() .ok_or_else(|| PlatformError::EventLoop("Event loop already taken".to_string()))?; use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent}; - + let mut cursor_position = winit::dpi::PhysicalPosition::new(0.0, 0.0); let mut scale_factor = 1.0; @@ -62,35 +74,50 @@ impl Platform for DesktopPlatform { elwt.set_control_flow(winit::event_loop::ControlFlow::Wait); match event { - WinitEvent::WindowEvent { event, .. } => { - match event { - WinitWindowEvent::CursorMoved { position, device_id, .. } => { - cursor_position = position; - if let Some(strato_event) = crate::event_loop::convert_window_event( - WinitWindowEvent::CursorMoved { position, device_id }, - cursor_position, - scale_factor - ) { - callback(strato_event); - } + WinitEvent::WindowEvent { event, .. } => match event { + WinitWindowEvent::CursorMoved { + position, + device_id, + .. + } => { + cursor_position = position; + if let Some(strato_event) = crate::event_loop::convert_window_event( + WinitWindowEvent::CursorMoved { + position, + device_id, + }, + cursor_position, + scale_factor, + ) { + callback(strato_event); } - WinitWindowEvent::ScaleFactorChanged { scale_factor: sf, inner_size_writer } => { - scale_factor = sf; - if let Some(strato_event) = crate::event_loop::convert_window_event( - WinitWindowEvent::ScaleFactorChanged { scale_factor: sf, inner_size_writer }, - cursor_position, - scale_factor - ) { - callback(strato_event); - } + } + WinitWindowEvent::ScaleFactorChanged { + scale_factor: sf, + inner_size_writer, + } => { + scale_factor = sf; + if let Some(strato_event) = crate::event_loop::convert_window_event( + WinitWindowEvent::ScaleFactorChanged { + scale_factor: sf, + inner_size_writer, + }, + cursor_position, + scale_factor, + ) { + callback(strato_event); } - _ => { - if let Some(strato_event) = crate::event_loop::convert_window_event(event, cursor_position, scale_factor) { - callback(strato_event); - } + } + _ => { + if let Some(strato_event) = crate::event_loop::convert_window_event( + event, + cursor_position, + scale_factor, + ) { + callback(strato_event); } } - } + }, WinitEvent::AboutToWait => { // All events have been processed } diff --git a/crates/strato-platform/src/event_loop.rs b/crates/strato-platform/src/event_loop.rs index 0913520..2284091 100644 --- a/crates/strato-platform/src/event_loop.rs +++ b/crates/strato-platform/src/event_loop.rs @@ -1,14 +1,16 @@ //! Event loop management +use crate::Application; use std::cell::RefCell; -use std::time::{Duration, Instant}; -use strato_core::event::{Event, MouseButton, MouseEvent, KeyboardEvent, KeyCode, Modifiers, WindowEvent}; -use std::sync::Arc; use std::rc::Rc; -use winit::window::Window; -use crate::Application; -use strato_renderer::Backend; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use strato_core::event::{ + Event, KeyCode, KeyboardEvent, Modifiers, MouseButton, MouseEvent, WindowEvent, +}; use strato_renderer::backend::WgpuBackend; +use strato_renderer::Backend; +use winit::window::Window; /// Custom event for the event loop #[derive(Debug)] @@ -58,14 +60,14 @@ impl EventLoop { #[cfg(not(target_arch = "wasm32"))] pub fn new() -> Result { use winit::event_loop::EventLoopBuilder; - + let inner = EventLoopBuilder::with_user_event() .build() .map_err(|_| EventLoopError::CreationFailed)?; - + Ok(Self { inner }) } - + #[cfg(target_arch = "wasm32")] pub fn new() -> Result { Ok(Self { @@ -81,13 +83,11 @@ impl EventLoop { inner: self.inner.create_proxy(), } } - + #[cfg(target_arch = "wasm32")] { let (sender, _) = mpsc::channel(); - EventLoopProxy { - sender, - } + EventLoopProxy { sender } } } @@ -99,68 +99,88 @@ impl EventLoop { { use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent}; use winit::event_loop::ControlFlow; - + let mut last_update = Instant::now(); let mut cursor_position = winit::dpi::PhysicalPosition::new(0.0, 0.0); let mut scale_factor = 1.0; - - self.inner.run(move |event, elwt| { - match event { - WinitEvent::WindowEvent { event, .. } => { - match event { - WinitWindowEvent::CloseRequested => { - elwt.exit(); - } - WinitWindowEvent::RedrawRequested => { - // Handle redraw - emit a custom redraw event - handler(Event::Window(WindowEvent::Resize { width: 0, height: 0 })); - } - WinitWindowEvent::CursorMoved { position, device_id, .. } => { - cursor_position = position; - if let Some(strato_event) = convert_window_event( - WinitWindowEvent::CursorMoved { position, device_id }, - cursor_position, - scale_factor - ) { - handler(strato_event); + + self.inner + .run(move |event, elwt| { + match event { + WinitEvent::WindowEvent { event, .. } => { + match event { + WinitWindowEvent::CloseRequested => { + elwt.exit(); } - } - WinitWindowEvent::ScaleFactorChanged { scale_factor: sf, inner_size_writer } => { - scale_factor = sf; - if let Some(strato_event) = convert_window_event( - WinitWindowEvent::ScaleFactorChanged { scale_factor: sf, inner_size_writer }, - cursor_position, - scale_factor - ) { - handler(strato_event); + WinitWindowEvent::RedrawRequested => { + // Handle redraw - emit a custom redraw event + handler(Event::Window(WindowEvent::Resize { + width: 0, + height: 0, + })); } - } - _ => { - if let Some(strato_event) = convert_window_event(event, cursor_position, scale_factor) { - handler(strato_event); + WinitWindowEvent::CursorMoved { + position, + device_id, + .. + } => { + cursor_position = position; + if let Some(strato_event) = convert_window_event( + WinitWindowEvent::CursorMoved { + position, + device_id, + }, + cursor_position, + scale_factor, + ) { + handler(strato_event); + } + } + WinitWindowEvent::ScaleFactorChanged { + scale_factor: sf, + inner_size_writer, + } => { + scale_factor = sf; + if let Some(strato_event) = convert_window_event( + WinitWindowEvent::ScaleFactorChanged { + scale_factor: sf, + inner_size_writer, + }, + cursor_position, + scale_factor, + ) { + handler(strato_event); + } + } + _ => { + if let Some(strato_event) = + convert_window_event(event, cursor_position, scale_factor) + { + handler(strato_event); + } } } } - } - WinitEvent::AboutToWait => { - // Implement frame rate limiting - let now = Instant::now(); - let frame_time = Duration::from_millis(16); // ~60 FPS - - if now.duration_since(last_update) >= frame_time { - last_update = now; - elwt.set_control_flow(ControlFlow::Poll); - } else { - elwt.set_control_flow(ControlFlow::WaitUntil(last_update + frame_time)); + WinitEvent::AboutToWait => { + // Implement frame rate limiting + let now = Instant::now(); + let frame_time = Duration::from_millis(16); // ~60 FPS + + if now.duration_since(last_update) >= frame_time { + last_update = now; + elwt.set_control_flow(ControlFlow::Poll); + } else { + elwt.set_control_flow(ControlFlow::WaitUntil(last_update + frame_time)); + } } + WinitEvent::UserEvent(custom) => { + handler(Event::Custom(Arc::new(custom.event))); + } + _ => {} } - WinitEvent::UserEvent(custom) => { - handler(Event::Custom(Arc::new(custom.event))); - } - _ => {} - } - }).expect("Event loop failed"); - + }) + .expect("Event loop failed"); + // This should never be reached, but if it is, exit the process std::process::exit(0); } @@ -177,75 +197,93 @@ impl EventLoop { { use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent}; use winit::event_loop::ControlFlow; - + let state = Rc::new(RefCell::new(AppState::new())); - - self.inner.run(move |event, event_loop_window_target| { - event_loop_window_target.set_control_flow(ControlFlow::Poll); - - let mut state = state.borrow_mut(); - - match event { - WinitEvent::Resumed => { - if !state.window_created { - let window = Arc::new( - window_builder - .build_winit(event_loop_window_target) - .expect("Failed to create window"), - ); - - state.winit_window = Some(window); - state.window_created = true; - state.needs_redraw = true; - state.last_update = Instant::now(); + + self.inner + .run(move |event, event_loop_window_target| { + event_loop_window_target.set_control_flow(ControlFlow::Poll); + + let mut state = state.borrow_mut(); + + match event { + WinitEvent::Resumed => { + if !state.window_created { + let window = Arc::new( + window_builder + .build_winit(event_loop_window_target) + .expect("Failed to create window"), + ); + + state.winit_window = Some(window); + state.window_created = true; + state.needs_redraw = true; + state.last_update = Instant::now(); + } } - } - WinitEvent::WindowEvent { event, .. } => { - match event { - WinitWindowEvent::CursorMoved { position, device_id, .. } => { + WinitEvent::WindowEvent { event, .. } => match event { + WinitWindowEvent::CursorMoved { + position, + device_id, + .. + } => { state.cursor_position = position; if let Some(strato_event) = convert_window_event( - WinitWindowEvent::CursorMoved { position, device_id }, + WinitWindowEvent::CursorMoved { + position, + device_id, + }, state.cursor_position, - state.scale_factor + state.scale_factor, ) { handler(strato_event); } } - WinitWindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer } => { + WinitWindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer, + } => { state.scale_factor = scale_factor; if let Some(strato_event) = convert_window_event( - WinitWindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer }, + WinitWindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer, + }, state.cursor_position, - state.scale_factor + state.scale_factor, ) { handler(strato_event); } } _ => { - if let Some(strato_event) = convert_window_event(event, state.cursor_position, state.scale_factor) { + if let Some(strato_event) = convert_window_event( + event, + state.cursor_position, + state.scale_factor, + ) { handler(strato_event); } } - } - } - WinitEvent::AboutToWait => { - let now = Instant::now(); - let frame_time = Duration::from_millis(16); - - if state.needs_redraw && now.duration_since(state.last_update) >= frame_time { - if let Some(ref window) = state.winit_window { - window.request_redraw(); - state.last_update = now; + }, + WinitEvent::AboutToWait => { + let now = Instant::now(); + let frame_time = Duration::from_millis(16); + + if state.needs_redraw && now.duration_since(state.last_update) >= frame_time + { + if let Some(ref window) = state.winit_window { + window.request_redraw(); + state.last_update = now; + } } } + WinitEvent::UserEvent(custom) => { + handler(Event::Custom(Arc::new(custom.event))); + } + _ => {} } - WinitEvent::UserEvent(custom) => { - handler(Event::Custom(Arc::new(custom.event))); - } - _ => {} - } - }).map_err(|_| EventLoopError::RunFailed) + }) + .map_err(|_| EventLoopError::RunFailed) } /// Run the event loop with a window and application @@ -260,171 +298,200 @@ impl EventLoop { F: FnMut(Event) + 'static, { use winit::event::{Event as WinitEvent, WindowEvent}; - + let app_state = Rc::new(RefCell::new(AppState::new())); let mut handler = handler; - + // Store the application in the state app_state.borrow_mut().app = Some(app); - - self.inner.run(move |event, event_loop_window_target| { - let mut state = app_state.borrow_mut(); - - match event { - WinitEvent::Resumed => { - if !state.window_created { - let window = Arc::new( - window_builder - .build_winit(event_loop_window_target) - .expect("Failed to create window"), - ); - - // Store the window first - state.winit_window = Some(window.clone()); - - // Initialize Backend - println!("=== INITIALIZING BACKEND ==="); - let mut backend = Box::new(WgpuBackend::new()); - pollster::block_on(backend.init(&*window)).expect("Failed to init backend"); - - // Set initial scale factor - let scale_factor = window.scale_factor(); - backend.set_scale_factor(scale_factor); - state.scale_factor = scale_factor; - - state.backend = Some(backend); - - state.renderer_initialized = true; - state.window_created = true; - state.needs_redraw = true; - state.last_update = Instant::now(); - } - } - WinitEvent::WindowEvent { event, .. } => { - match event { - WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - println!("Scale factor changed to: {}", scale_factor); + + self.inner + .run(move |event, event_loop_window_target| { + let mut state = app_state.borrow_mut(); + + match event { + WinitEvent::Resumed => { + if !state.window_created { + let window = Arc::new( + window_builder + .build_winit(event_loop_window_target) + .expect("Failed to create window"), + ); + + // Store the window first + state.winit_window = Some(window.clone()); + + // Initialize Backend + println!("=== INITIALIZING BACKEND ==="); + let mut backend = Box::new(WgpuBackend::new()); + pollster::block_on(backend.init(&*window)) + .expect("Failed to init backend"); + + // Set initial scale factor + let scale_factor = window.scale_factor(); + backend.set_scale_factor(scale_factor); state.scale_factor = scale_factor; - if let Some(backend) = &mut state.backend { - backend.set_scale_factor(scale_factor); - } + + state.backend = Some(backend); + + state.renderer_initialized = true; + state.window_created = true; + state.needs_redraw = true; + state.last_update = Instant::now(); } - WindowEvent::CursorMoved { position, device_id, .. } => { - state.cursor_position = position; - if let Some(strato_event) = convert_window_event( - WindowEvent::CursorMoved { position, device_id }, - state.cursor_position, - state.scale_factor - ) { - // println!("EventLoop: CursorMoved to {:?} (Logical), Scale: {}", strato_event, state.scale_factor); - if let Some(app) = &mut state.app { - app.handle_event(strato_event.clone()); + } + WinitEvent::WindowEvent { event, .. } => { + match event { + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + println!("Scale factor changed to: {}", scale_factor); + state.scale_factor = scale_factor; + if let Some(backend) = &mut state.backend { + backend.set_scale_factor(scale_factor); } - handler(strato_event); } - } - WindowEvent::Resized(physical_size) => { - // Resize the backend when the window is resized - if let Some(backend) = &mut state.backend { - backend.resize(physical_size.width, physical_size.height); + WindowEvent::CursorMoved { + position, + device_id, + .. + } => { + state.cursor_position = position; + if let Some(strato_event) = convert_window_event( + WindowEvent::CursorMoved { + position, + device_id, + }, + state.cursor_position, + state.scale_factor, + ) { + // println!("EventLoop: CursorMoved to {:?} (Logical), Scale: {}", strato_event, state.scale_factor); + if let Some(app) = &mut state.app { + app.handle_event(strato_event.clone()); + } + handler(strato_event); + } } - - let event = strato_core::event::Event::Window(strato_core::event::WindowEvent::Resize { - width: physical_size.width, - height: physical_size.height, - }); - - if let Some(app) = &mut state.app { - app.handle_event(event.clone()); + WindowEvent::Resized(physical_size) => { + // Resize the backend when the window is resized + if let Some(backend) = &mut state.backend { + backend.resize(physical_size.width, physical_size.height); + } + + let event = strato_core::event::Event::Window( + strato_core::event::WindowEvent::Resize { + width: physical_size.width, + height: physical_size.height, + }, + ); + + if let Some(app) = &mut state.app { + app.handle_event(event.clone()); + } + handler(event); } - handler(event); - } - WindowEvent::RedrawRequested => { - state.needs_redraw = false; - - // Get window size before borrowing app - let (physical_width, physical_height) = if let Some(window) = &state.winit_window { - let size = window.inner_size(); - (size.width as f32, size.height as f32) - } else { - (800.0, 600.0) // Default fallback - }; - - // Get scale factor - let scale_factor = if let Some(window) = &state.winit_window { - window.scale_factor() as f32 - } else { - 1.0 - }; - - // Use logical size for layout - let logical_width = physical_width / scale_factor; - let logical_height = physical_height / scale_factor; - - // Call the application's render method and get the render batch - if let Some(app) = &mut state.app { - if let Err(e) = app.render_simple(logical_width, logical_height) { - eprintln!("Render error: {}", e); + WindowEvent::RedrawRequested => { + state.needs_redraw = false; + + // Get window size before borrowing app + let (physical_width, physical_height) = + if let Some(window) = &state.winit_window { + let size = window.inner_size(); + (size.width as f32, size.height as f32) + } else { + (800.0, 600.0) // Default fallback + }; + + // Get scale factor + let scale_factor = if let Some(window) = &state.winit_window { + window.scale_factor() as f32 } else { - // Get the render batch - if let Some(batch) = app.get_render_batch() { - // Use Backend - if let Some(backend) = &mut state.backend { - if let Err(e) = backend.begin_frame() { - tracing::error!("Backend begin_frame error: {}", e); - } else { - // Submit the batch directly using the optimized path - if let Err(e) = backend.submit_batch(&batch) { - tracing::error!("Backend submit_batch error: {}", e); - } - - if let Err(e) = backend.end_frame() { - tracing::error!("Backend end_frame error: {}", e); + 1.0 + }; + + // Use logical size for layout + let logical_width = physical_width / scale_factor; + let logical_height = physical_height / scale_factor; + + // Call the application's render method and get the render batch + if let Some(app) = &mut state.app { + if let Err(e) = app.render_simple(logical_width, logical_height) + { + eprintln!("Render error: {}", e); + } else { + // Get the render batch + if let Some(batch) = app.get_render_batch() { + // Use Backend + if let Some(backend) = &mut state.backend { + if let Err(e) = backend.begin_frame() { + tracing::error!( + "Backend begin_frame error: {}", + e + ); + } else { + // Submit the batch directly using the optimized path + if let Err(e) = backend.submit_batch(&batch) { + tracing::error!( + "Backend submit_batch error: {}", + e + ); + } + + if let Err(e) = backend.end_frame() { + tracing::error!( + "Backend end_frame error: {}", + e + ); + } } } } } } } - } - WindowEvent::CloseRequested => { - let event = strato_core::event::Event::Window(strato_core::event::WindowEvent::Close); - if let Some(app) = &mut state.app { - app.handle_event(event.clone()); - } - handler(event); - event_loop_window_target.exit(); - } - _ => { - if let Some(strato_event) = convert_window_event(event, state.cursor_position, state.scale_factor) { - // println!("EventLoop: Event {:?}", strato_event); + WindowEvent::CloseRequested => { + let event = strato_core::event::Event::Window( + strato_core::event::WindowEvent::Close, + ); if let Some(app) = &mut state.app { - app.handle_event(strato_event.clone()); + app.handle_event(event.clone()); + } + handler(event); + event_loop_window_target.exit(); + } + _ => { + if let Some(strato_event) = convert_window_event( + event, + state.cursor_position, + state.scale_factor, + ) { + // println!("EventLoop: Event {:?}", strato_event); + if let Some(app) = &mut state.app { + app.handle_event(strato_event.clone()); + } + handler(strato_event); } - handler(strato_event); } } } - } - WinitEvent::AboutToWait => { - // Always request redraw to maintain continuous rendering - if state.renderer_initialized { - if let Some(window) = &state.winit_window { - window.request_redraw(); + WinitEvent::AboutToWait => { + // Always request redraw to maintain continuous rendering + if state.renderer_initialized { + if let Some(window) = &state.winit_window { + window.request_redraw(); + } + state.needs_redraw = true; // Keep requesting redraws } - state.needs_redraw = true; // Keep requesting redraws } - } - WinitEvent::UserEvent(custom_event) => { - if let Some(app) = &mut state.app { - app.handle_event(custom_event.event.clone()); + WinitEvent::UserEvent(custom_event) => { + if let Some(app) = &mut state.app { + app.handle_event(custom_event.event.clone()); + } + handler(custom_event.event); } - handler(custom_event.event); + _ => {} } - _ => {} - } - }).expect("Event loop failed"); - + }) + .expect("Event loop failed"); + Ok(()) } @@ -436,14 +503,14 @@ impl EventLoop { { use wasm_bindgen::prelude::*; use web_sys::{window, Document, Element}; - + // Set up web event listeners let window = window().expect("should have a window in this context"); let document = window.document().expect("window should have a document"); - + // Mouse events let handler_clone = std::rc::Rc::new(std::cell::RefCell::new(handler)); - + // Example: mouse click handler let handler_ref = handler_clone.clone(); let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { @@ -460,35 +527,38 @@ impl EventLoop { }; handler_ref.borrow_mut()(Event::MouseDown(mouse_event)); }) as Box); - - document.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()) + + document + .add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()) .expect("should register click handler"); closure.forget(); - + // Start the animation loop self.start_animation_loop(); } - + #[cfg(target_arch = "wasm32")] fn start_animation_loop(&self) { use wasm_bindgen::prelude::*; use web_sys::window; - + let f = std::rc::Rc::new(std::cell::RefCell::new(None)); let g = f.clone(); - + *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { // Animation frame logic here - + // Schedule next frame if let Some(window) = window() { - window.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref()) + window + .request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref()) .expect("should register animation frame"); } }) as Box)); - + if let Some(window) = window() { - window.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref()) + window + .request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref()) .expect("should register animation frame"); } } @@ -513,20 +583,20 @@ impl EventLoopProxy { pub fn send_event(&self, event: Event) -> Result<(), Box> { #[cfg(not(target_arch = "wasm32"))] { - self.inner.send_event(CustomEvent { event }) + self.inner + .send_event(CustomEvent { event }) .map_err(|e| Box::new(e) as Box) } - + #[cfg(target_arch = "wasm32")] { - self.sender.send(event) + self.sender + .send(event) .map_err(|e| Box::new(e) as Box) } } } - - /// Event loop error #[derive(Debug, thiserror::Error)] pub enum EventLoopError { @@ -545,24 +615,21 @@ pub fn convert_window_event( cursor_position: winit::dpi::PhysicalPosition, scale_factor: f64, ) -> Option { - use winit::event::{WindowEvent as WE, MouseButton as MB, ElementState}; use glam::Vec2; - + use winit::event::{ElementState, MouseButton as MB, WindowEvent as WE}; + match event { WE::CloseRequested => Some(Event::Window(WindowEvent::Close)), - + WE::Resized(size) => Some(Event::Window(WindowEvent::Resize { width: size.width, height: size.height, })), - - WE::Moved(pos) => Some(Event::Window(WindowEvent::Move { - x: pos.x, - y: pos.y, - })), - + + WE::Moved(pos) => Some(Event::Window(WindowEvent::Move { x: pos.x, y: pos.y })), + WE::Focused(focused) => Some(Event::Window(WindowEvent::Focus(focused))), - + WE::CursorMoved { position, .. } => { let logical_x = position.x / scale_factor; let logical_y = position.y / scale_factor; @@ -572,8 +639,8 @@ pub fn convert_window_event( modifiers: Modifiers::default(), delta: Vec2::ZERO, })) - }, - + } + WE::MouseInput { state, button, .. } => { let button = match button { MB::Left => MouseButton::Left, @@ -583,10 +650,10 @@ pub fn convert_window_event( MB::Forward => MouseButton::Other(4), MB::Other(n) => MouseButton::Other(n), }; - + let logical_x = cursor_position.x / scale_factor; let logical_y = cursor_position.y / scale_factor; - + match state { ElementState::Pressed => Some(Event::MouseDown(MouseEvent { position: Vec2::new(logical_x as f32, logical_y as f32), @@ -601,24 +668,30 @@ pub fn convert_window_event( delta: Vec2::ZERO, })), } - }, - + } + WE::MouseWheel { delta, .. } => { let delta_vec = match delta { winit::event::MouseScrollDelta::LineDelta(x, y) => Vec2::new(x * 20.0, y * 20.0), - winit::event::MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32), + winit::event::MouseScrollDelta::PixelDelta(pos) => { + Vec2::new(pos.x as f32, pos.y as f32) + } }; - + Some(Event::MouseWheel { delta: delta_vec, modifiers: Modifiers::default(), }) } - - WE::KeyboardInput { device_id: _, event, is_synthetic: _ } => { + + WE::KeyboardInput { + device_id: _, + event, + is_synthetic: _, + } => { if let winit::keyboard::PhysicalKey::Code(keycode) = event.physical_key { let key_code = convert_physical_key_code(keycode); - + match event.state { ElementState::Pressed => Some(Event::KeyDown(KeyboardEvent { key_code, @@ -637,11 +710,9 @@ pub fn convert_window_event( None } } - - WE::Ime(winit::event::Ime::Commit(text)) => { - Some(Event::TextInput(text)) - } - + + WE::Ime(winit::event::Ime::Commit(text)) => Some(Event::TextInput(text)), + _ => None, } } @@ -650,52 +721,82 @@ pub fn convert_window_event( #[cfg(not(target_arch = "wasm32"))] fn convert_physical_key_code(keycode: winit::keyboard::KeyCode) -> KeyCode { use winit::keyboard::KeyCode as WK; - + match keycode { - WK::KeyA => KeyCode::A, WK::KeyB => KeyCode::B, WK::KeyC => KeyCode::C, - WK::KeyD => KeyCode::D, WK::KeyE => KeyCode::E, WK::KeyF => KeyCode::F, - WK::KeyG => KeyCode::G, WK::KeyH => KeyCode::H, WK::KeyI => KeyCode::I, - WK::KeyJ => KeyCode::J, WK::KeyK => KeyCode::K, WK::KeyL => KeyCode::L, - WK::KeyM => KeyCode::M, WK::KeyN => KeyCode::N, WK::KeyO => KeyCode::O, - WK::KeyP => KeyCode::P, WK::KeyQ => KeyCode::Q, WK::KeyR => KeyCode::R, - WK::KeyS => KeyCode::S, WK::KeyT => KeyCode::T, WK::KeyU => KeyCode::U, - WK::KeyV => KeyCode::V, WK::KeyW => KeyCode::W, WK::KeyX => KeyCode::X, - WK::KeyY => KeyCode::Y, WK::KeyZ => KeyCode::Z, - - WK::Digit0 => KeyCode::Num0, WK::Digit1 => KeyCode::Num1, - WK::Digit2 => KeyCode::Num2, WK::Digit3 => KeyCode::Num3, - WK::Digit4 => KeyCode::Num4, WK::Digit5 => KeyCode::Num5, - WK::Digit6 => KeyCode::Num6, WK::Digit7 => KeyCode::Num7, - WK::Digit8 => KeyCode::Num8, WK::Digit9 => KeyCode::Num9, - - WK::F1 => KeyCode::F1, WK::F2 => KeyCode::F2, WK::F3 => KeyCode::F3, - WK::F4 => KeyCode::F4, WK::F5 => KeyCode::F5, WK::F6 => KeyCode::F6, - WK::F7 => KeyCode::F7, WK::F8 => KeyCode::F8, WK::F9 => KeyCode::F9, - WK::F10 => KeyCode::F10, WK::F11 => KeyCode::F11, WK::F12 => KeyCode::F12, - + WK::KeyA => KeyCode::A, + WK::KeyB => KeyCode::B, + WK::KeyC => KeyCode::C, + WK::KeyD => KeyCode::D, + WK::KeyE => KeyCode::E, + WK::KeyF => KeyCode::F, + WK::KeyG => KeyCode::G, + WK::KeyH => KeyCode::H, + WK::KeyI => KeyCode::I, + WK::KeyJ => KeyCode::J, + WK::KeyK => KeyCode::K, + WK::KeyL => KeyCode::L, + WK::KeyM => KeyCode::M, + WK::KeyN => KeyCode::N, + WK::KeyO => KeyCode::O, + WK::KeyP => KeyCode::P, + WK::KeyQ => KeyCode::Q, + WK::KeyR => KeyCode::R, + WK::KeyS => KeyCode::S, + WK::KeyT => KeyCode::T, + WK::KeyU => KeyCode::U, + WK::KeyV => KeyCode::V, + WK::KeyW => KeyCode::W, + WK::KeyX => KeyCode::X, + WK::KeyY => KeyCode::Y, + WK::KeyZ => KeyCode::Z, + + WK::Digit0 => KeyCode::Num0, + WK::Digit1 => KeyCode::Num1, + WK::Digit2 => KeyCode::Num2, + WK::Digit3 => KeyCode::Num3, + WK::Digit4 => KeyCode::Num4, + WK::Digit5 => KeyCode::Num5, + WK::Digit6 => KeyCode::Num6, + WK::Digit7 => KeyCode::Num7, + WK::Digit8 => KeyCode::Num8, + WK::Digit9 => KeyCode::Num9, + + WK::F1 => KeyCode::F1, + WK::F2 => KeyCode::F2, + WK::F3 => KeyCode::F3, + WK::F4 => KeyCode::F4, + WK::F5 => KeyCode::F5, + WK::F6 => KeyCode::F6, + WK::F7 => KeyCode::F7, + WK::F8 => KeyCode::F8, + WK::F9 => KeyCode::F9, + WK::F10 => KeyCode::F10, + WK::F11 => KeyCode::F11, + WK::F12 => KeyCode::F12, + WK::Enter => KeyCode::Enter, WK::Escape => KeyCode::Escape, WK::Backspace => KeyCode::Backspace, WK::Tab => KeyCode::Tab, WK::Space => KeyCode::Space, - + WK::ArrowLeft => KeyCode::Left, WK::ArrowRight => KeyCode::Right, WK::ArrowUp => KeyCode::Up, WK::ArrowDown => KeyCode::Down, - + WK::ShiftLeft | WK::ShiftRight => KeyCode::Shift, WK::ControlLeft | WK::ControlRight => KeyCode::Control, WK::AltLeft | WK::AltRight => KeyCode::Alt, WK::SuperLeft | WK::SuperRight => KeyCode::Super, - + WK::Delete => KeyCode::Delete, WK::Insert => KeyCode::Insert, WK::Home => KeyCode::Home, WK::End => KeyCode::End, WK::PageUp => KeyCode::PageUp, WK::PageDown => KeyCode::PageDown, - + _ => KeyCode::A, // Default fallback } } diff --git a/crates/strato-platform/src/init.rs b/crates/strato-platform/src/init.rs index 25b4c24..7ce4ebc 100644 --- a/crates/strato-platform/src/init.rs +++ b/crates/strato-platform/src/init.rs @@ -4,7 +4,7 @@ //! allowing developers to customize font loading, logging, and other aspects //! of the framework initialization. -use std::sync::{Arc, RwLock, OnceLock}; +use std::sync::{Arc, OnceLock, RwLock}; use strato_core::{Result, StratoError}; use strato_renderer::text::TextRenderer; @@ -95,14 +95,20 @@ impl InitBuilder { } if self.config.enable_logging { - strato_core::strato_trace!(strato_core::logging::LogCategory::Core, "Initializing widgets..."); + strato_core::strato_trace!( + strato_core::logging::LogCategory::Core, + "Initializing widgets..." + ); } strato_widgets::init()?; self.widgets_initialized = true; if self.config.enable_logging { - strato_core::strato_trace!(strato_core::logging::LogCategory::Core, "Widgets initialized"); + strato_core::strato_trace!( + strato_core::logging::LogCategory::Core, + "Widgets initialized" + ); } Ok(self) @@ -119,11 +125,16 @@ impl InitBuilder { } if self.config.enable_logging { - strato_core::strato_trace!(strato_core::logging::LogCategory::Platform, "Initializing platform with font optimizations..."); + strato_core::strato_trace!( + strato_core::logging::LogCategory::Platform, + "Initializing platform with font optimizations..." + ); } // Initialize platform with our custom configuration - crate::init().map_err(|e| strato_core::StratoError::platform(format!("Platform init failed: {}", e)))?; + crate::init().map_err(|e| { + strato_core::StratoError::platform(format!("Platform init failed: {}", e)) + })?; // Initialize the global text renderer with optimizations self.init_optimized_text_renderer()?; @@ -131,7 +142,10 @@ impl InitBuilder { self.platform_initialized = true; if self.config.enable_logging { - strato_core::strato_trace!(strato_core::logging::LogCategory::Platform, "Platform initialized"); + strato_core::strato_trace!( + strato_core::logging::LogCategory::Platform, + "Platform initialized" + ); } Ok(self) @@ -139,9 +153,7 @@ impl InitBuilder { /// Initialize all modules at once pub fn init_all(&mut self) -> Result<()> { - self.init_core()? - .init_widgets()? - .init_platform()?; + self.init_core()?.init_widgets()?.init_platform()?; Ok(()) } @@ -152,10 +164,14 @@ impl InitBuilder { } // Create optimized text renderer - strato_core::strato_trace!(strato_core::logging::LogCategory::Platform, "Creating optimized TextRenderer"); + strato_core::strato_trace!( + strato_core::logging::LogCategory::Platform, + "Creating optimized TextRenderer" + ); let text_renderer = TextRenderer::new(); - GLOBAL_TEXT_RENDERER.set(Arc::new(RwLock::new(text_renderer))) + GLOBAL_TEXT_RENDERER + .set(Arc::new(RwLock::new(text_renderer))) .map_err(|_| StratoError::platform("Failed to set global text renderer".to_string()))?; Ok(()) @@ -173,8 +189,6 @@ pub fn get_text_renderer() -> Option>> { GLOBAL_TEXT_RENDERER.get().cloned() } - - /// Convenience function for full initialization with default config pub fn init_all() -> Result<()> { InitBuilder::new().init_all() @@ -209,4 +223,4 @@ mod tests { assert!(!builder.widgets_initialized); assert!(!builder.platform_initialized); } -} \ No newline at end of file +} diff --git a/crates/strato-platform/src/lib.rs b/crates/strato-platform/src/lib.rs index 6ec9698..c3316e3 100644 --- a/crates/strato-platform/src/lib.rs +++ b/crates/strato-platform/src/lib.rs @@ -2,9 +2,9 @@ //! //! Provides cross-platform window management and event handling. -pub mod window; -pub mod event_loop; pub mod application; +pub mod event_loop; +pub mod window; #[cfg(not(target_arch = "wasm32"))] pub mod desktop; @@ -12,9 +12,9 @@ pub mod desktop; #[cfg(target_arch = "wasm32")] pub mod web; -pub use window::{Window, WindowBuilder, WindowId}; -pub use event_loop::{EventLoop, EventLoopProxy}; pub use application::{Application, ApplicationBuilder}; +pub use event_loop::{EventLoop, EventLoopProxy}; +pub use window::{Window, WindowBuilder, WindowId}; use strato_core::event::Event; @@ -23,13 +23,13 @@ use strato_core::event::Event; pub enum PlatformError { #[error("Window creation failed: {0}")] WindowCreation(String), - + #[error("Event loop error: {0}")] EventLoop(String), - + #[error("Platform not supported")] Unsupported, - + #[error("WebAssembly error: {0}")] #[cfg(target_arch = "wasm32")] Wasm(String), @@ -38,17 +38,22 @@ pub enum PlatformError { /// Platform trait for OS-specific implementations pub trait Platform { /// Initialize the platform - fn init() -> Result where Self: Sized; - + fn init() -> Result + where + Self: Sized; + /// Create a window fn create_window(&mut self, builder: WindowBuilder) -> Result; - + /// Run the event loop - fn run_event_loop(&mut self, callback: Box) -> Result<(), PlatformError>; - + fn run_event_loop( + &mut self, + callback: Box, + ) -> Result<(), PlatformError>; + /// Request a redraw fn request_redraw(&self, window_id: WindowId); - + /// Exit the application fn exit(&mut self); } @@ -59,7 +64,7 @@ pub fn current_platform() -> Box { { Box::new(desktop::DesktopPlatform::new()) } - + #[cfg(target_arch = "wasm32")] { Box::new(web::WebPlatform::new()) @@ -71,11 +76,11 @@ pub mod init; /// Initialize the platform layer pub fn init() -> Result<(), PlatformError> { tracing::info!("StratoUI Platform initialized"); - + #[cfg(target_arch = "wasm32")] { console_error_panic_hook::set_once(); } - + Ok(()) } diff --git a/crates/strato-platform/src/web.rs b/crates/strato-platform/src/web.rs index 666d68c..f37961e 100644 --- a/crates/strato-platform/src/web.rs +++ b/crates/strato-platform/src/web.rs @@ -23,8 +23,7 @@ impl WebPlatform { /// Get the web window fn web_window() -> Result { - web_sys::window() - .ok_or_else(|| PlatformError::Wasm("Failed to get window".to_string())) + web_sys::window().ok_or_else(|| PlatformError::Wasm("Failed to get window".to_string())) } /// Get the document @@ -37,7 +36,7 @@ impl WebPlatform { /// Create a canvas element fn create_canvas(builder: &WindowBuilder) -> Result { let document = Self::document()?; - + let canvas = document .create_element("canvas") .map_err(|e| PlatformError::Wasm(format!("Failed to create canvas: {:?}", e)))? @@ -52,7 +51,7 @@ impl WebPlatform { let style = canvas.style(); style.set_property("display", "block").ok(); style.set_property("margin", "0 auto").ok(); - + // Append to body document .body() @@ -66,7 +65,7 @@ impl WebPlatform { /// Setup event listeners fn setup_event_listeners(canvas: &HtmlCanvasElement) -> Result<(), PlatformError> { let canvas_clone = canvas.clone(); - + // Mouse move let mouse_move = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { // Handle mouse move @@ -74,9 +73,12 @@ impl WebPlatform { let _y = event.offset_y(); // TODO: Dispatch to event handler }) as Box); - - canvas.add_event_listener_with_callback("mousemove", mouse_move.as_ref().unchecked_ref()) - .map_err(|e| PlatformError::Wasm(format!("Failed to add mousemove listener: {:?}", e)))?; + + canvas + .add_event_listener_with_callback("mousemove", mouse_move.as_ref().unchecked_ref()) + .map_err(|e| { + PlatformError::Wasm(format!("Failed to add mousemove listener: {:?}", e)) + })?; mouse_move.forget(); // Mouse down @@ -85,9 +87,12 @@ impl WebPlatform { let _button = event.button(); // TODO: Dispatch to event handler }) as Box); - - canvas.add_event_listener_with_callback("mousedown", mouse_down.as_ref().unchecked_ref()) - .map_err(|e| PlatformError::Wasm(format!("Failed to add mousedown listener: {:?}", e)))?; + + canvas + .add_event_listener_with_callback("mousedown", mouse_down.as_ref().unchecked_ref()) + .map_err(|e| { + PlatformError::Wasm(format!("Failed to add mousedown listener: {:?}", e)) + })?; mouse_down.forget(); // Mouse up @@ -96,8 +101,9 @@ impl WebPlatform { let _button = event.button(); // TODO: Dispatch to event handler }) as Box); - - canvas.add_event_listener_with_callback("mouseup", mouse_up.as_ref().unchecked_ref()) + + canvas + .add_event_listener_with_callback("mouseup", mouse_up.as_ref().unchecked_ref()) .map_err(|e| PlatformError::Wasm(format!("Failed to add mouseup listener: {:?}", e)))?; mouse_up.forget(); @@ -107,7 +113,7 @@ impl WebPlatform { let _key = event.key(); // TODO: Dispatch to event handler }) as Box); - + Self::document()? .add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref()) .map_err(|e| PlatformError::Wasm(format!("Failed to add keydown listener: {:?}", e)))?; @@ -118,7 +124,7 @@ impl WebPlatform { let _key = event.key(); // TODO: Dispatch to event handler }) as Box); - + Self::document()? .add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref()) .map_err(|e| PlatformError::Wasm(format!("Failed to add keyup listener: {:?}", e)))?; @@ -129,7 +135,7 @@ impl WebPlatform { // Handle resize // TODO: Dispatch resize event }) as Box); - + Self::web_window()? .add_event_listener_with_callback("resize", resize.as_ref().unchecked_ref()) .map_err(|e| PlatformError::Wasm(format!("Failed to add resize listener: {:?}", e)))?; @@ -143,7 +149,7 @@ impl Platform for WebPlatform { fn init() -> Result { // Set panic hook for better error messages console_error_panic_hook::set_once(); - + Ok(Self::new()) } @@ -161,7 +167,7 @@ impl Platform for WebPlatform { let window_id = self.window_id; self.window_id += 1; - + self.canvas = Some(canvas.clone()); Ok(Window { @@ -176,24 +182,24 @@ impl Platform for WebPlatform { { // In web, the event loop is handled by the browser // We use requestAnimationFrame for the render loop - + let window = Self::web_window()?; let callback = std::rc::Rc::new(std::cell::RefCell::new(callback)); - + // Animation frame loop let f = std::rc::Rc::new(std::cell::RefCell::new(None)); let g = f.clone(); - + *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { // Request next frame request_animation_frame(f.borrow().as_ref().unwrap()); - + // Handle frame update // TODO: Dispatch update event }) as Box)); - + request_animation_frame(g.borrow().as_ref().unwrap()); - + Ok(()) } diff --git a/crates/strato-platform/src/window.rs b/crates/strato-platform/src/window.rs index 86c0e2b..5608659 100644 --- a/crates/strato-platform/src/window.rs +++ b/crates/strato-platform/src/window.rs @@ -1,7 +1,7 @@ //! Window management -use strato_core::{Size, types::Point}; use std::sync::Arc; +use strato_core::{types::Point, Size}; /// Window identifier pub type WindowId = u64; @@ -34,9 +34,7 @@ impl Window { Size::new(size.width as f32, size.height as f32) } #[cfg(target_arch = "wasm32")] - WindowInner::Web(canvas) => { - Size::new(canvas.width() as f32, canvas.height() as f32) - } + WindowInner::Web(canvas) => Size::new(canvas.width() as f32, canvas.height() as f32), } } @@ -45,10 +43,8 @@ impl Window { match &self.inner { #[cfg(not(target_arch = "wasm32"))] WindowInner::Desktop(window) => { - let _ = window.request_inner_size(winit::dpi::LogicalSize::new( - size.width, - size.height, - )); + let _ = window + .request_inner_size(winit::dpi::LogicalSize::new(size.width, size.height)); } #[cfg(target_arch = "wasm32")] WindowInner::Web(canvas) => { @@ -62,11 +58,10 @@ impl Window { pub fn position(&self) -> Point { match &self.inner { #[cfg(not(target_arch = "wasm32"))] - WindowInner::Desktop(window) => { - window.outer_position() - .map(|pos| Point::new(pos.x as f32, pos.y as f32)) - .unwrap_or(Point::zero()) - } + WindowInner::Desktop(window) => window + .outer_position() + .map(|pos| Point::new(pos.x as f32, pos.y as f32)) + .unwrap_or(Point::zero()), #[cfg(target_arch = "wasm32")] WindowInner::Web(_) => Point::zero(), } @@ -186,25 +181,37 @@ impl WindowBuilder { /// Build winit window #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn build_winit(&self, event_loop: &winit::event_loop::EventLoopWindowTarget) -> Result { + pub(crate) fn build_winit( + &self, + event_loop: &winit::event_loop::EventLoopWindowTarget, + ) -> Result { let mut builder = winit::window::WindowBuilder::new() .with_title(&self.title) - .with_inner_size(winit::dpi::LogicalSize::new(self.size.width, self.size.height)) + .with_inner_size(winit::dpi::LogicalSize::new( + self.size.width, + self.size.height, + )) .with_resizable(self.resizable) .with_decorations(self.decorations) .with_transparent(self.transparent) - .with_window_level(if self.always_on_top { winit::window::WindowLevel::AlwaysOnTop } else { winit::window::WindowLevel::Normal }); + .with_window_level(if self.always_on_top { + winit::window::WindowLevel::AlwaysOnTop + } else { + winit::window::WindowLevel::Normal + }); if let Some(pos) = self.position { builder = builder.with_position(winit::dpi::LogicalPosition::new(pos.x, pos.y)); } if let Some(min) = self.min_size { - builder = builder.with_min_inner_size(winit::dpi::LogicalSize::new(min.width, min.height)); + builder = + builder.with_min_inner_size(winit::dpi::LogicalSize::new(min.width, min.height)); } if let Some(max) = self.max_size { - builder = builder.with_max_inner_size(winit::dpi::LogicalSize::new(max.width, max.height)); + builder = + builder.with_max_inner_size(winit::dpi::LogicalSize::new(max.width, max.height)); } if self.fullscreen { diff --git a/crates/strato-renderer/src/backend/mod.rs b/crates/strato-renderer/src/backend/mod.rs index dcba649..1169b2a 100644 --- a/crates/strato-renderer/src/backend/mod.rs +++ b/crates/strato-renderer/src/backend/mod.rs @@ -1,6 +1,6 @@ +use self::commands::RenderCommand; use anyhow::Result; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use self::commands::RenderCommand; pub mod commands; pub mod wgpu; @@ -30,10 +30,12 @@ pub trait Backend: Send + Sync { /// Submit a render batch for execution (optimized path) fn submit_batch(&mut self, _batch: &crate::batch::RenderBatch) -> Result<()> { - // Default implementation falls back to submit if possible, or errors? - // Since RenderBatch contains DrawCommands which are not exactly RenderCommands (DrawCommand vs RenderCommand), - // we can't easily fallback without conversion logic. - // Let's make it mandatory or return logic error. - Err(anyhow::anyhow!("submit_batch not implemented for this backend")) + // Default implementation falls back to submit if possible, or errors? + // Since RenderBatch contains DrawCommands which are not exactly RenderCommands (DrawCommand vs RenderCommand), + // we can't easily fallback without conversion logic. + // Let's make it mandatory or return logic error. + Err(anyhow::anyhow!( + "submit_batch not implemented for this backend" + )) } } diff --git a/crates/strato-renderer/src/backend/wgpu.rs b/crates/strato-renderer/src/backend/wgpu.rs index e251688..a64a847 100644 --- a/crates/strato-renderer/src/backend/wgpu.rs +++ b/crates/strato-renderer/src/backend/wgpu.rs @@ -1,12 +1,12 @@ -use anyhow::Result; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; -use wgpu::{Backends, CommandEncoderDescriptor, Surface}; -use crate::backend::{Backend, commands::RenderCommand}; +use crate::backend::{commands::RenderCommand, Backend}; use crate::gpu::{ - DeviceManager, SurfaceManager, ShaderManager, BufferManager, TextureManager, PipelineManager, - SimpleVertex + BufferManager, DeviceManager, PipelineManager, ShaderManager, SimpleVertex, SurfaceManager, + TextureManager, }; +use anyhow::Result; use async_trait::async_trait; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use wgpu::{Backends, CommandEncoderDescriptor, Surface}; pub struct WgpuBackend { device_mgr: Option, @@ -15,10 +15,10 @@ pub struct WgpuBackend { buffer_mgr: Option, texture_mgr: Option, pipeline_mgr: Option, - + // State scale_factor: f64, - + // Cache for reuse vertices: Vec, indices: Vec, @@ -44,26 +44,21 @@ impl WgpuBackend { W: HasWindowHandle + HasDisplayHandle + Send + Sync, { println!("=== INITIALIZING WGPU BACKEND ==="); - + // 1. Initialize DeviceManager let device_mgr = DeviceManager::new(Backends::all()).await?; println!("✅ DeviceManager initialized"); - + // 2. Create Surface - // Safety: The surface must live as long as the window. + // Safety: The surface must live as long as the window. // We assume the window outlives the backend. let surface = device_mgr.instance().create_surface(window)?; let surface: Surface<'static> = unsafe { std::mem::transmute(surface) }; - + // 3. Initialize SurfaceManager // We use a default size, it will be resized later - let surface_mgr = SurfaceManager::new( - surface, - device_mgr.device(), - device_mgr.adapter(), - 800, - 600, - )?; + let surface_mgr = + SurfaceManager::new(surface, device_mgr.device(), device_mgr.adapter(), 800, 600)?; println!("✅ SurfaceManager initialized"); // 4. Initialize ShaderManager @@ -95,7 +90,8 @@ impl WgpuBackend { // Initialize projection matrix let width = surface_mgr.width(); let height = surface_mgr.height(); - let projection = glam::Mat4::orthographic_rh(0.0, width as f32, height as f32, 0.0, -1.0, 1.0); + let projection = + glam::Mat4::orthographic_rh(0.0, width as f32, height as f32, 0.0, -1.0, 1.0); buffer_mgr.upload_projection(device_mgr.queue(), &projection.to_cols_array_2d()); self.device_mgr = Some(device_mgr); @@ -111,35 +107,54 @@ impl WgpuBackend { #[async_trait] impl Backend for WgpuBackend { - fn resize(&mut self, width: u32, height: u32) { - if let (Some(surface_mgr), Some(device_mgr), Some(buffer_mgr)) = (&mut self.surface_mgr, &self.device_mgr, &mut self.buffer_mgr) { + if let (Some(surface_mgr), Some(device_mgr), Some(buffer_mgr)) = ( + &mut self.surface_mgr, + &self.device_mgr, + &mut self.buffer_mgr, + ) { if let Err(e) = surface_mgr.resize(width, height, device_mgr.device()) { eprintln!("Failed to resize surface: {}", e); } - + // Update projection matrix using logical coordinates // This ensures that the UI coordinates (which are logical) map correctly to the physical viewport let logical_width = width as f64 / self.scale_factor; let logical_height = height as f64 / self.scale_factor; - - let projection = glam::Mat4::orthographic_rh(0.0, logical_width as f32, logical_height as f32, 0.0, -1.0, 1.0); + + let projection = glam::Mat4::orthographic_rh( + 0.0, + logical_width as f32, + logical_height as f32, + 0.0, + -1.0, + 1.0, + ); buffer_mgr.upload_projection(device_mgr.queue(), &projection.to_cols_array_2d()); } } fn set_scale_factor(&mut self, scale_factor: f64) { self.scale_factor = scale_factor; - + // Update projection matrix if initialized - if let (Some(surface_mgr), Some(device_mgr), Some(buffer_mgr)) = (&self.surface_mgr, &self.device_mgr, &mut self.buffer_mgr) { + if let (Some(surface_mgr), Some(device_mgr), Some(buffer_mgr)) = + (&self.surface_mgr, &self.device_mgr, &mut self.buffer_mgr) + { let width = surface_mgr.width(); let height = surface_mgr.height(); - + let logical_width = width as f64 / self.scale_factor; let logical_height = height as f64 / self.scale_factor; - - let projection = glam::Mat4::orthographic_rh(0.0, logical_width as f32, logical_height as f32, 0.0, -1.0, 1.0); + + let projection = glam::Mat4::orthographic_rh( + 0.0, + logical_width as f32, + logical_height as f32, + 0.0, + -1.0, + 1.0, + ); buffer_mgr.upload_projection(device_mgr.queue(), &projection.to_cols_array_2d()); } } @@ -156,25 +171,39 @@ impl Backend for WgpuBackend { } fn submit(&mut self, commands: &[RenderCommand]) -> Result<()> { - let device_mgr = self.device_mgr.as_ref().ok_or_else(|| anyhow::anyhow!("DeviceManager not initialized"))?; - let texture_mgr = self.texture_mgr.as_mut().ok_or_else(|| anyhow::anyhow!("TextureManager not initialized"))?; - let pipeline_mgr = self.pipeline_mgr.as_ref().ok_or_else(|| anyhow::anyhow!("PipelineManager not initialized"))?; - let buffer_mgr = self.buffer_mgr.as_mut().ok_or_else(|| anyhow::anyhow!("BufferManager not initialized"))?; - let surface_mgr = self.surface_mgr.as_mut().ok_or_else(|| anyhow::anyhow!("SurfaceManager not initialized"))?; + let device_mgr = self + .device_mgr + .as_ref() + .ok_or_else(|| anyhow::anyhow!("DeviceManager not initialized"))?; + let texture_mgr = self + .texture_mgr + .as_mut() + .ok_or_else(|| anyhow::anyhow!("TextureManager not initialized"))?; + let pipeline_mgr = self + .pipeline_mgr + .as_ref() + .ok_or_else(|| anyhow::anyhow!("PipelineManager not initialized"))?; + let buffer_mgr = self + .buffer_mgr + .as_mut() + .ok_or_else(|| anyhow::anyhow!("BufferManager not initialized"))?; + let surface_mgr = self + .surface_mgr + .as_mut() + .ok_or_else(|| anyhow::anyhow!("SurfaceManager not initialized"))?; // 1. Clear buffers self.vertices.clear(); self.indices.clear(); - + let mut vertex_count = 0; let mut current_index_start = 0; let mut current_index_count = 0; let mut batches: Vec = Vec::new(); let mut scissor_stack: Vec<[u32; 4]> = Vec::new(); - - let get_current_scissor = |stack: &[ [u32; 4] ]| -> Option<[u32; 4]> { - stack.last().cloned() - }; + + let get_current_scissor = + |stack: &[[u32; 4]]| -> Option<[u32; 4]> { stack.last().cloned() }; // 2. Process commands for cmd in commands { @@ -201,14 +230,22 @@ impl Backend for WgpuBackend { let min_y = y.max(0); let max_x = (x + w).min(surface_w).max(min_x); let max_y = (y + h).min(surface_h).max(min_y); - let mut new_rect = [min_x as u32, min_y as u32, (max_x - min_x) as u32, (max_y - min_y) as u32]; + let mut new_rect = [ + min_x as u32, + min_y as u32, + (max_x - min_x) as u32, + (max_y - min_y) as u32, + ]; if let Some(parent) = scissor_stack.last() { - let px = parent[0]; let py = parent[1]; let pw = parent[2]; let ph = parent[3]; - let ix = new_rect[0].max(px); - let iy = new_rect[1].max(py); - let iw = (new_rect[0] + new_rect[2]).min(px + pw).saturating_sub(ix); - let ih = (new_rect[1] + new_rect[3]).min(py + ph).saturating_sub(iy); - new_rect = [ix, iy, iw, ih]; + let px = parent[0]; + let py = parent[1]; + let pw = parent[2]; + let ph = parent[3]; + let ix = new_rect[0].max(px); + let iy = new_rect[1].max(py); + let iw = (new_rect[0] + new_rect[2]).min(px + pw).saturating_sub(ix); + let ih = (new_rect[1] + new_rect[3]).min(py + ph).saturating_sub(iy); + new_rect = [ix, iy, iw, ih]; } scissor_stack.push(new_rect); } @@ -224,7 +261,11 @@ impl Backend for WgpuBackend { } scissor_stack.pop(); } - RenderCommand::DrawRect { rect, color, transform } => { + RenderCommand::DrawRect { + rect, + color, + transform, + } => { // Implementation as before... let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); let transform = transform.unwrap_or(strato_core::types::Transform::identity()); @@ -238,18 +279,40 @@ impl Backend for WgpuBackend { let p2 = apply_transform([x + w, y + h]); let p3 = apply_transform([x, y + h]); let color_arr = [color.r, color.g, color.b, color.a]; - - self.vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p0, color_arr))); - self.vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p1, color_arr))); - self.vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p2, color_arr))); - self.vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p3, color_arr))); - - self.indices.push(vertex_count); self.indices.push(vertex_count + 1); self.indices.push(vertex_count + 2); - self.indices.push(vertex_count); self.indices.push(vertex_count + 2); self.indices.push(vertex_count + 3); + + self.vertices + .push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p0, color_arr, + ))); + self.vertices + .push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p1, color_arr, + ))); + self.vertices + .push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p2, color_arr, + ))); + self.vertices + .push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p3, color_arr, + ))); + + self.indices.push(vertex_count); + self.indices.push(vertex_count + 1); + self.indices.push(vertex_count + 2); + self.indices.push(vertex_count); + self.indices.push(vertex_count + 2); + self.indices.push(vertex_count + 3); vertex_count += 4; current_index_count += 6; } - RenderCommand::DrawText { text, position, color, font_size, align } => { + RenderCommand::DrawText { + text, + position, + color, + font_size, + align, + } => { // Copied implementation from before... let (x_orig, y) = *position; let color_arr = [color.r, color.g, color.b, color.a]; @@ -258,62 +321,141 @@ impl Backend for WgpuBackend { let text_width = if align != strato_core::text::TextAlign::Left { let mut width = 0.0; for ch in text.chars() { - if let Some(glyph) = texture_mgr.get_or_cache_glyph(device_mgr.queue(), ch, font_size as u32) { width += glyph.metrics.advance; } - else if ch == ' ' { width += font_size * 0.3; } + if let Some(glyph) = texture_mgr.get_or_cache_glyph( + device_mgr.queue(), + ch, + font_size as u32, + ) { + width += glyph.metrics.advance; + } else if ch == ' ' { + width += font_size * 0.3; + } } width - } else { 0.0 }; + } else { + 0.0 + }; let mut x = x_orig; - if align == strato_core::text::TextAlign::Center { x -= text_width / 2.0; } - else if align == strato_core::text::TextAlign::Right { x -= text_width; } - + if align == strato_core::text::TextAlign::Center { + x -= text_width / 2.0; + } else if align == strato_core::text::TextAlign::Right { + x -= text_width; + } + for ch in text.chars() { - if let Some(glyph) = texture_mgr.get_or_cache_glyph(device_mgr.queue(), ch, font_size as u32) { - let (gx, gy, w, h) = (x + glyph.metrics.bearing_x as f32, y + font_size - glyph.metrics.bearing_y as f32, glyph.metrics.width as f32, glyph.metrics.height as f32); + if let Some(glyph) = + texture_mgr.get_or_cache_glyph(device_mgr.queue(), ch, font_size as u32) + { + let (gx, gy, w, h) = ( + x + glyph.metrics.bearing_x as f32, + y + font_size - glyph.metrics.bearing_y as f32, + glyph.metrics.width as f32, + glyph.metrics.height as f32, + ); let (u0, v0, u1, v1) = glyph.uv_rect; - let p0 = [gx, gy]; let p1 = [gx + w, gy]; let p2 = [gx + w, gy + h]; let p3 = [gx, gy + h]; - self.vertices.push(SimpleVertex { position: p0, color: color_arr, uv: [u0, v0], params: [0.0; 4], flags: 1 }); - self.vertices.push(SimpleVertex { position: p1, color: color_arr, uv: [u1, v0], params: [0.0; 4], flags: 1 }); - self.vertices.push(SimpleVertex { position: p2, color: color_arr, uv: [u1, v1], params: [0.0; 4], flags: 1 }); - self.vertices.push(SimpleVertex { position: p3, color: color_arr, uv: [u0, v1], params: [0.0; 4], flags: 1 }); - self.indices.push(vertex_count); self.indices.push(vertex_count + 1); self.indices.push(vertex_count + 2); - self.indices.push(vertex_count); self.indices.push(vertex_count + 2); self.indices.push(vertex_count + 3); + let p0 = [gx, gy]; + let p1 = [gx + w, gy]; + let p2 = [gx + w, gy + h]; + let p3 = [gx, gy + h]; + self.vertices.push(SimpleVertex { + position: p0, + color: color_arr, + uv: [u0, v0], + params: [0.0; 4], + flags: 1, + }); + self.vertices.push(SimpleVertex { + position: p1, + color: color_arr, + uv: [u1, v0], + params: [0.0; 4], + flags: 1, + }); + self.vertices.push(SimpleVertex { + position: p2, + color: color_arr, + uv: [u1, v1], + params: [0.0; 4], + flags: 1, + }); + self.vertices.push(SimpleVertex { + position: p3, + color: color_arr, + uv: [u0, v1], + params: [0.0; 4], + flags: 1, + }); + self.indices.push(vertex_count); + self.indices.push(vertex_count + 1); + self.indices.push(vertex_count + 2); + self.indices.push(vertex_count); + self.indices.push(vertex_count + 2); + self.indices.push(vertex_count + 3); vertex_count += 4; current_index_count += 6; x += glyph.metrics.advance; - } else if ch == ' ' { x += font_size * 0.3; } + } else if ch == ' ' { + x += font_size * 0.3; + } } } _ => {} } } - + // Push final batch if current_index_count > 0 { - batches.push(DrawBatch { index_start: current_index_start, index_count: current_index_count, scissor: get_current_scissor(&scissor_stack) }); + batches.push(DrawBatch { + index_start: current_index_start, + index_count: current_index_count, + scissor: get_current_scissor(&scissor_stack), + }); } - - Self::flush_and_render(batches, device_mgr, surface_mgr, buffer_mgr, pipeline_mgr, &self.vertices, &self.indices) + + Self::flush_and_render( + batches, + device_mgr, + surface_mgr, + buffer_mgr, + pipeline_mgr, + &self.vertices, + &self.indices, + ) } fn submit_batch(&mut self, batch: &crate::batch::RenderBatch) -> Result<()> { - let device_mgr = self.device_mgr.as_ref().ok_or_else(|| anyhow::anyhow!("DeviceManager not initialized"))?; - let texture_mgr = self.texture_mgr.as_mut().ok_or_else(|| anyhow::anyhow!("TextureManager not initialized"))?; - let pipeline_mgr = self.pipeline_mgr.as_ref().ok_or_else(|| anyhow::anyhow!("PipelineManager not initialized"))?; - let buffer_mgr = self.buffer_mgr.as_mut().ok_or_else(|| anyhow::anyhow!("BufferManager not initialized"))?; - let surface_mgr = self.surface_mgr.as_mut().ok_or_else(|| anyhow::anyhow!("SurfaceManager not initialized"))?; + let device_mgr = self + .device_mgr + .as_ref() + .ok_or_else(|| anyhow::anyhow!("DeviceManager not initialized"))?; + let texture_mgr = self + .texture_mgr + .as_mut() + .ok_or_else(|| anyhow::anyhow!("TextureManager not initialized"))?; + let pipeline_mgr = self + .pipeline_mgr + .as_ref() + .ok_or_else(|| anyhow::anyhow!("PipelineManager not initialized"))?; + let buffer_mgr = self + .buffer_mgr + .as_mut() + .ok_or_else(|| anyhow::anyhow!("BufferManager not initialized"))?; + let surface_mgr = self + .surface_mgr + .as_mut() + .ok_or_else(|| anyhow::anyhow!("SurfaceManager not initialized"))?; // 1. Clear buffers self.vertices.clear(); self.indices.clear(); - + // 2. Pre-populate vertices from batch // We convert them to SimpleVertex self.vertices.reserve(batch.vertices.len()); for v in &batch.vertices { self.vertices.push(SimpleVertex::from(v)); } - + // 3. Process commands let mut batches: Vec = Vec::new(); let mut current_index_start = 0; @@ -321,9 +463,8 @@ impl Backend for WgpuBackend { let mut scissor_stack: Vec<[u32; 4]> = Vec::new(); let mut vertex_count = self.vertices.len() as u32; // Offset for new vertices (text) - let get_current_scissor = |stack: &[ [u32; 4] ]| -> Option<[u32; 4]> { - stack.last().cloned() - }; + let get_current_scissor = + |stack: &[[u32; 4]]| -> Option<[u32; 4]> { stack.last().cloned() }; // Combine commands and overlay_commands for processing let all_commands = batch.commands.iter().chain(batch.overlay_commands.iter()); @@ -333,11 +474,15 @@ impl Backend for WgpuBackend { match cmd { DrawCommand::PushClip(rect) => { if current_index_count > 0 { - batches.push(DrawBatch { index_start: current_index_start, index_count: current_index_count, scissor: get_current_scissor(&scissor_stack) }); + batches.push(DrawBatch { + index_start: current_index_start, + index_count: current_index_count, + scissor: get_current_scissor(&scissor_stack), + }); current_index_start += current_index_count; current_index_count = 0; } - // Calculate scissor + // Calculate scissor let scale = self.scale_factor; let x = (rect.x as f64 * scale).round() as i32; let y = (rect.y as f64 * scale).round() as i32; @@ -349,29 +494,41 @@ impl Backend for WgpuBackend { let min_y = y.max(0); let max_x = (x + w).min(surface_w).max(min_x); let max_y = (y + h).min(surface_h).max(min_y); - let mut new_rect = [min_x as u32, min_y as u32, (max_x - min_x) as u32, (max_y - min_y) as u32]; + let mut new_rect = [ + min_x as u32, + min_y as u32, + (max_x - min_x) as u32, + (max_y - min_y) as u32, + ]; if let Some(parent) = scissor_stack.last() { - let px = parent[0]; let py = parent[1]; let pw = parent[2]; let ph = parent[3]; - let ix = new_rect[0].max(px); - let iy = new_rect[1].max(py); - let iw = (new_rect[0] + new_rect[2]).min(px + pw).saturating_sub(ix); - let ih = (new_rect[1] + new_rect[3]).min(py + ph).saturating_sub(iy); - new_rect = [ix, iy, iw, ih]; + let px = parent[0]; + let py = parent[1]; + let pw = parent[2]; + let ph = parent[3]; + let ix = new_rect[0].max(px); + let iy = new_rect[1].max(py); + let iw = (new_rect[0] + new_rect[2]).min(px + pw).saturating_sub(ix); + let ih = (new_rect[1] + new_rect[3]).min(py + ph).saturating_sub(iy); + new_rect = [ix, iy, iw, ih]; } scissor_stack.push(new_rect); } DrawCommand::PopClip => { if current_index_count > 0 { - batches.push(DrawBatch { index_start: current_index_start, index_count: current_index_count, scissor: get_current_scissor(&scissor_stack) }); + batches.push(DrawBatch { + index_start: current_index_start, + index_count: current_index_count, + scissor: get_current_scissor(&scissor_stack), + }); current_index_start += current_index_count; current_index_count = 0; } scissor_stack.pop(); } - DrawCommand::Rect { index_range, .. } | - DrawCommand::TexturedQuad { index_range, .. } | - DrawCommand::Circle { index_range, .. } | - DrawCommand::Line { index_range, .. } => { + DrawCommand::Rect { index_range, .. } + | DrawCommand::TexturedQuad { index_range, .. } + | DrawCommand::Circle { index_range, .. } + | DrawCommand::Line { index_range, .. } => { // Use pre-batched indices // We need to copy indices from batch.indices[index_range] to self.indices // self.vertices already contains batch vertices at offset 0 @@ -379,74 +536,155 @@ impl Backend for WgpuBackend { // So we can use them directly (just cast to u32) for i in index_range.clone() { if (i as usize) < batch.indices.len() { - self.indices.push(batch.indices[i as usize] as u32); - current_index_count += 1; + self.indices.push(batch.indices[i as usize] as u32); + current_index_count += 1; } } } - DrawCommand::Text { text, position, color, font_size, align, .. } => { + DrawCommand::Text { + text, + position, + color, + font_size, + align, + .. + } => { // Generate text vertices/indices immediate mode style // Appending to self.vertices, so indices start at 'vertex_count' let (x_orig, y) = *position; let color_arr = [color.r, color.g, color.b, color.a]; let font_size = *font_size; let align = *align; - let text_width = if align != strato_core::text::TextAlign::Left { + let text_width = if align != strato_core::text::TextAlign::Left { let mut width = 0.0; for ch in text.chars() { - if let Some(glyph) = texture_mgr.get_or_cache_glyph(device_mgr.queue(), ch, font_size as u32) { width += glyph.metrics.advance; } - else if ch == ' ' { width += font_size * 0.3; } + if let Some(glyph) = texture_mgr.get_or_cache_glyph( + device_mgr.queue(), + ch, + font_size as u32, + ) { + width += glyph.metrics.advance; + } else if ch == ' ' { + width += font_size * 0.3; + } } width - } else { 0.0 }; + } else { + 0.0 + }; let mut x = x_orig; - if align == strato_core::text::TextAlign::Center { x -= text_width / 2.0; } - else if align == strato_core::text::TextAlign::Right { x -= text_width; } - + if align == strato_core::text::TextAlign::Center { + x -= text_width / 2.0; + } else if align == strato_core::text::TextAlign::Right { + x -= text_width; + } + for ch in text.chars() { - if let Some(glyph) = texture_mgr.get_or_cache_glyph(device_mgr.queue(), ch, font_size as u32) { - let (gx, gy, w, h) = (x + glyph.metrics.bearing_x as f32, y + font_size - glyph.metrics.bearing_y as f32, glyph.metrics.width as f32, glyph.metrics.height as f32); + if let Some(glyph) = + texture_mgr.get_or_cache_glyph(device_mgr.queue(), ch, font_size as u32) + { + let (gx, gy, w, h) = ( + x + glyph.metrics.bearing_x as f32, + y + font_size - glyph.metrics.bearing_y as f32, + glyph.metrics.width as f32, + glyph.metrics.height as f32, + ); let (u0, v0, u1, v1) = glyph.uv_rect; - let p0 = [gx, gy]; let p1 = [gx + w, gy]; let p2 = [gx + w, gy + h]; let p3 = [gx, gy + h]; - self.vertices.push(SimpleVertex { position: p0, color: color_arr, uv: [u0, v0], params: [0.0; 4], flags: 1 }); - self.vertices.push(SimpleVertex { position: p1, color: color_arr, uv: [u1, v0], params: [0.0; 4], flags: 1 }); - self.vertices.push(SimpleVertex { position: p2, color: color_arr, uv: [u1, v1], params: [0.0; 4], flags: 1 }); - self.vertices.push(SimpleVertex { position: p3, color: color_arr, uv: [u0, v1], params: [0.0; 4], flags: 1 }); - self.indices.push(vertex_count); self.indices.push(vertex_count + 1); self.indices.push(vertex_count + 2); - self.indices.push(vertex_count); self.indices.push(vertex_count + 2); self.indices.push(vertex_count + 3); + let p0 = [gx, gy]; + let p1 = [gx + w, gy]; + let p2 = [gx + w, gy + h]; + let p3 = [gx, gy + h]; + self.vertices.push(SimpleVertex { + position: p0, + color: color_arr, + uv: [u0, v0], + params: [0.0; 4], + flags: 1, + }); + self.vertices.push(SimpleVertex { + position: p1, + color: color_arr, + uv: [u1, v0], + params: [0.0; 4], + flags: 1, + }); + self.vertices.push(SimpleVertex { + position: p2, + color: color_arr, + uv: [u1, v1], + params: [0.0; 4], + flags: 1, + }); + self.vertices.push(SimpleVertex { + position: p3, + color: color_arr, + uv: [u0, v1], + params: [0.0; 4], + flags: 1, + }); + self.indices.push(vertex_count); + self.indices.push(vertex_count + 1); + self.indices.push(vertex_count + 2); + self.indices.push(vertex_count); + self.indices.push(vertex_count + 2); + self.indices.push(vertex_count + 3); vertex_count += 4; current_index_count += 6; x += glyph.metrics.advance; - } else if ch == ' ' { x += font_size * 0.3; } + } else if ch == ' ' { + x += font_size * 0.3; + } } } _ => {} } } - - if current_index_count > 0 { - batches.push(DrawBatch { index_start: current_index_start, index_count: current_index_count, scissor: get_current_scissor(&scissor_stack) }); + + if current_index_count > 0 { + batches.push(DrawBatch { + index_start: current_index_start, + index_count: current_index_count, + scissor: get_current_scissor(&scissor_stack), + }); } - Self::flush_and_render(batches, device_mgr, surface_mgr, buffer_mgr, pipeline_mgr, &self.vertices, &self.indices) + Self::flush_and_render( + batches, + device_mgr, + surface_mgr, + buffer_mgr, + pipeline_mgr, + &self.vertices, + &self.indices, + ) } - - } impl WgpuBackend { - fn flush_and_render(batches: Vec, device_mgr: &DeviceManager, surface_mgr: &mut SurfaceManager, buffer_mgr: &mut BufferManager, pipeline_mgr: &PipelineManager, vertices: &[SimpleVertex], indices: &[u32]) -> Result<()> { - // 3. Update buffers + fn flush_and_render( + batches: Vec, + device_mgr: &DeviceManager, + surface_mgr: &mut SurfaceManager, + buffer_mgr: &mut BufferManager, + pipeline_mgr: &PipelineManager, + vertices: &[SimpleVertex], + indices: &[u32], + ) -> Result<()> { + // 3. Update buffers buffer_mgr.upload_vertices(device_mgr.device(), device_mgr.queue(), vertices); buffer_mgr.upload_indices(device_mgr.device(), device_mgr.queue(), indices); // 4. Render Pass let output = surface_mgr.get_current_texture()?; - let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let mut encoder = device_mgr.device().create_command_encoder(&CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = device_mgr + .device() + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -455,7 +693,12 @@ impl WgpuBackend { view: &view, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }), + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.1, + b: 0.1, + a: 1.0, + }), store: wgpu::StoreOp::Store, }, })], @@ -468,18 +711,35 @@ impl WgpuBackend { render_pass.set_pipeline(pipeline_mgr.pipeline()); render_pass.set_bind_group(0, pipeline_mgr.bind_group(), &[]); render_pass.set_vertex_buffer(0, buffer_mgr.vertex_buffer().slice(..)); - render_pass.set_index_buffer(buffer_mgr.index_buffer().slice(..), wgpu::IndexFormat::Uint32); - + render_pass.set_index_buffer( + buffer_mgr.index_buffer().slice(..), + wgpu::IndexFormat::Uint32, + ); + for batch in batches { - if batch.index_count == 0 { continue; } - if let Some(scissor) = batch.scissor { - if scissor[2] == 0 || scissor[3] == 0 { continue; } - render_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); - } else { - render_pass.set_scissor_rect(0, 0, surface_mgr.width(), surface_mgr.height()); - } - render_pass.draw_indexed(batch.index_start .. batch.index_start + batch.index_count, 0, 0..1); - } + if batch.index_count == 0 { + continue; + } + if let Some(scissor) = batch.scissor { + if scissor[2] == 0 || scissor[3] == 0 { + continue; + } + render_pass + .set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); + } else { + render_pass.set_scissor_rect( + 0, + 0, + surface_mgr.width(), + surface_mgr.height(), + ); + } + render_pass.draw_indexed( + batch.index_start..batch.index_start + batch.index_count, + 0, + 0..1, + ); + } } } device_mgr.queue().submit(std::iter::once(encoder.finish())); @@ -493,4 +753,3 @@ struct DrawBatch { index_count: u32, scissor: Option<[u32; 4]>, } - diff --git a/crates/strato-renderer/src/batch.rs b/crates/strato-renderer/src/batch.rs index 8d1b3a9..193586f 100644 --- a/crates/strato-renderer/src/batch.rs +++ b/crates/strato-renderer/src/batch.rs @@ -1,10 +1,10 @@ //! Render batching system for efficient GPU rendering -use std::collections::HashMap; -use strato_core::types::{Color, Rect, Transform}; -use crate::vertex::Vertex; use crate::text::TextRenderer; +use crate::vertex::Vertex; +use std::collections::HashMap; use std::ops::Range; +use strato_core::types::{Color, Rect, Transform}; use strato_core::text::TextAlign; @@ -128,10 +128,10 @@ impl RenderBatch { let start_index = self.indices.len() as u32; self.batch_rect(rect, color, transform); let end_index = self.indices.len() as u32; - - let command = DrawCommand::Rect { - rect, - color, + + let command = DrawCommand::Rect { + rect, + color, transform, index_range: start_index..end_index, }; @@ -149,18 +149,51 @@ impl RenderBatch { } /// Add a rounded rectangle to the batch - pub fn add_rounded_rect(&mut self, rect: Rect, color: Color, radius: f32, transform: Transform) { - let command = DrawCommand::RoundedRect { rect, color, radius, transform }; + pub fn add_rounded_rect( + &mut self, + rect: Rect, + color: Color, + radius: f32, + transform: Transform, + ) { + let command = DrawCommand::RoundedRect { + rect, + color, + radius, + transform, + }; self.commands.push(command); } /// Add text to the batch - pub fn add_text(&mut self, text: String, position: (f32, f32), color: Color, font_size: f32, letter_spacing: f32) { - self.add_text_aligned(text, position, color, font_size, letter_spacing, TextAlign::Left); + pub fn add_text( + &mut self, + text: String, + position: (f32, f32), + color: Color, + font_size: f32, + letter_spacing: f32, + ) { + self.add_text_aligned( + text, + position, + color, + font_size, + letter_spacing, + TextAlign::Left, + ); } /// Add aligned text to the batch - pub fn add_text_aligned(&mut self, text: String, position: (f32, f32), color: Color, font_size: f32, letter_spacing: f32, align: TextAlign) { + pub fn add_text_aligned( + &mut self, + text: String, + position: (f32, f32), + color: Color, + font_size: f32, + letter_spacing: f32, + align: TextAlign, + ) { let command = DrawCommand::Text { text: text.clone(), position, @@ -174,19 +207,25 @@ impl RenderBatch { /// Add a rectangle to the overlay layer (drawn last) pub fn add_overlay_rect(&mut self, rect: Rect, color: Color, transform: Transform) { - - let command = DrawCommand::Rect { - rect, - color, + let command = DrawCommand::Rect { + rect, + color, transform, - index_range: 0..0 + index_range: 0..0, }; self.overlay_commands.push(command); - } /// Add aligned text to the overlay layer (drawn last) - pub fn add_overlay_text_aligned(&mut self, text: String, position: (f32, f32), color: Color, font_size: f32, letter_spacing: f32, align: TextAlign) { + pub fn add_overlay_text_aligned( + &mut self, + text: String, + position: (f32, f32), + color: Color, + font_size: f32, + letter_spacing: f32, + align: TextAlign, + ) { let command = DrawCommand::Text { text: text.clone(), position, @@ -245,12 +284,26 @@ impl RenderBatch { } /// Add a circle to the batch - pub fn add_circle(&mut self, center: (f32, f32), radius: f32, color: Color, segments: u32, transform: Transform) { + pub fn add_circle( + &mut self, + center: (f32, f32), + radius: f32, + color: Color, + segments: u32, + transform: Transform, + ) { let start_index = self.indices.len() as u32; self.batch_circle(center, radius, color, segments, transform); let end_index = self.indices.len() as u32; - let command = DrawCommand::Circle { center, radius, color, segments, transform, index_range: start_index..end_index }; + let command = DrawCommand::Circle { + center, + radius, + color, + segments, + transform, + index_range: start_index..end_index, + }; self.commands.push(command); } @@ -259,28 +312,34 @@ impl RenderBatch { let start_index = self.indices.len() as u32; self.batch_line(start, end, color, thickness); let end_index = self.indices.len() as u32; - - let command = DrawCommand::Line { start, end, color, thickness, index_range: start_index..end_index }; + + let command = DrawCommand::Line { + start, + end, + color, + thickness, + index_range: start_index..end_index, + }; self.commands.push(command); } /// Add raw vertices and indices to the batch pub fn add_vertices(&mut self, vertices: &[Vertex], indices: &[u16]) { let vertex_offset = self.vertices.len() as u16; - + // Add vertices self.vertices.extend_from_slice(vertices); - + // Add indices with offset for &index in indices { self.indices.push(vertex_offset + index); } - + self.vertex_count += vertices.len() as u16; } - + /// Batch text with real GPU glyph rendering (requires TextureManager access) - /// + /// /// This is the full implementation that renders actual glyphs from the font atlas #[allow(dead_code)] fn batch_text_gpu( @@ -294,40 +353,53 @@ impl RenderBatch { ) { let (mut x, y) = position; let color_arr = [color.r, color.g, color.b, color.a]; - + for ch in text.chars() { if let Some(glyph) = texture_mgr.get_or_cache_glyph(queue, ch, font_size) { // Calculate glyph position with bearing let glyph_x = x + glyph.metrics.bearing_x as f32; let glyph_y = y - glyph.metrics.bearing_y as f32; - + // Glyph dimensions let w = glyph.metrics.width as f32; let h = glyph.metrics.height as f32; - + // UV coordinates from atlas let (u0, v0, u1, v1) = glyph.uv_rect; - + // Create textured quad for this glyph let base_idx = self.vertex_count; - + // Add 4 vertices for the quad - self.vertices.push(Vertex::textured([glyph_x, glyph_y], [u0, v0], color_arr)); - self.vertices.push(Vertex::textured([glyph_x + w, glyph_y], [u1, v0], color_arr)); - self.vertices.push(Vertex::textured([glyph_x + w, glyph_y + h], [u1, v1], color_arr)); - self.vertices.push(Vertex::textured([glyph_x, glyph_y + h], [u0, v1], color_arr)); - + self.vertices + .push(Vertex::textured([glyph_x, glyph_y], [u0, v0], color_arr)); + self.vertices.push(Vertex::textured( + [glyph_x + w, glyph_y], + [u1, v0], + color_arr, + )); + self.vertices.push(Vertex::textured( + [glyph_x + w, glyph_y + h], + [u1, v1], + color_arr, + )); + self.vertices.push(Vertex::textured( + [glyph_x, glyph_y + h], + [u0, v1], + color_arr, + )); + // Add 2 triangles (6 indices) self.indices.push(base_idx); self.indices.push(base_idx + 1); self.indices.push(base_idx + 2); - + self.indices.push(base_idx); self.indices.push(base_idx + 2); self.indices.push(base_idx + 3); - + self.vertex_count += 4; - + // Advance to next character position x += glyph.metrics.advance; } else { @@ -340,7 +412,7 @@ impl RenderBatch { /// Batch a rectangle into vertices and indices fn batch_rect(&mut self, rect: Rect, color: Color, transform: Transform) { let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); - + // Apply transform to vertices let positions = [ self.apply_transform([x, y], transform), @@ -386,19 +458,23 @@ impl RenderBatch { // Add indices for two triangles let base = self.vertex_count; - self.indices.extend_from_slice(&[ - base, base + 1, base + 2, - base, base + 2, base + 3, - ]); + self.indices + .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]); self.vertex_count += 4; } /// Batch a textured quad - fn batch_textured_quad(&mut self, rect: Rect, uv_rect: Rect, color: Color, transform: Transform) { + fn batch_textured_quad( + &mut self, + rect: Rect, + uv_rect: Rect, + color: Color, + transform: Transform, + ) { let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); let (u, v, uw, vh) = (uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height); - + // Apply transform to vertices let positions = [ self.apply_transform([x, y], transform), @@ -442,18 +518,23 @@ impl RenderBatch { self.vertices.extend_from_slice(&vertices); let base = self.vertex_count; - self.indices.extend_from_slice(&[ - base, base + 1, base + 2, - base, base + 2, base + 3, - ]); + self.indices + .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]); self.vertex_count += 4; } /// Batch a circle - fn batch_circle(&mut self, center: (f32, f32), radius: f32, color: Color, segments: u32, transform: Transform) { + fn batch_circle( + &mut self, + center: (f32, f32), + radius: f32, + color: Color, + segments: u32, + transform: Transform, + ) { let (cx, cy) = center; - + // Center vertex self.vertices.push(Vertex { position: self.apply_transform([cx, cy], transform), @@ -471,7 +552,7 @@ impl RenderBatch { let angle = (i as f32 / segments as f32) * 2.0 * std::f32::consts::PI; let x = cx + radius * angle.cos(); let y = cy + radius * angle.sin(); - + self.vertices.push(Vertex { position: self.apply_transform([x, y], transform), uv: [0.5 + 0.5 * angle.cos(), 0.5 + 0.5 * angle.sin()], @@ -496,16 +577,16 @@ impl RenderBatch { fn batch_line(&mut self, start: (f32, f32), end: (f32, f32), color: Color, thickness: f32) { let (x1, y1) = start; let (x2, y2) = end; - + // Calculate line direction and perpendicular let dx = x2 - x1; let dy = y2 - y1; let length = (dx * dx + dy * dy).sqrt(); - + if length == 0.0 { return; } - + let nx = -dy / length * thickness * 0.5; let ny = dx / length * thickness * 0.5; @@ -544,10 +625,8 @@ impl RenderBatch { self.vertices.extend_from_slice(&vertices); let base = self.vertex_count; - self.indices.extend_from_slice(&[ - base, base + 1, base + 2, - base, base + 2, base + 3, - ]); + self.indices + .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]); self.vertex_count += 4; } @@ -576,8 +655,21 @@ impl RenderBatch { } /// Register a texture in the atlas - pub fn register_texture(&mut self, id: u32, width: u32, height: u32, format: wgpu::TextureFormat) { - self.texture_atlas.insert(id, TextureInfo { width, height, format }); + pub fn register_texture( + &mut self, + id: u32, + width: u32, + height: u32, + format: wgpu::TextureFormat, + ) { + self.texture_atlas.insert( + id, + TextureInfo { + width, + height, + format, + }, + ); } /// Get texture info @@ -595,8 +687,8 @@ impl Default for RenderBatch { #[cfg(test)] mod tests { use super::*; - use strato_core::types::Color; use glam::Vec2; + use strato_core::types::Color; #[test] fn test_batch_rect() { diff --git a/crates/strato-renderer/src/buffer.rs b/crates/strato-renderer/src/buffer.rs index 97c7e67..6f31909 100644 --- a/crates/strato-renderer/src/buffer.rs +++ b/crates/strato-renderer/src/buffer.rs @@ -10,15 +10,18 @@ //! - Performance profiling and analytics //! - Lock-free buffer operations where possible -use std::ops::Range; +use anyhow::{Context, Result}; +use parking_lot::{Mutex, RwLock}; +use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; -use std::sync::{Arc, atomic::{AtomicU64, AtomicUsize, AtomicBool, Ordering}}; +use std::ops::Range; +use std::sync::{ + atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, + Arc, +}; use std::time::{Duration, Instant}; -use parking_lot::{RwLock, Mutex}; -use wgpu::{Buffer, BufferUsages, BufferDescriptor}; -use anyhow::{Result, Context}; -use tracing::{info, warn, debug, instrument}; -use serde::{Serialize, Deserialize}; +use tracing::{debug, info, instrument, warn}; +use wgpu::{Buffer, BufferDescriptor, BufferUsages}; use crate::device::ManagedDevice; use crate::memory::{MemoryManager, MemoryTier}; @@ -84,19 +87,19 @@ pub struct BufferAllocation { pub struct BufferPool { device: Arc, _memory_manager: Arc>, - + // Pool storage by size and usage pools: RwLock>>>, - + // Active allocations active_allocations: RwLock>, - + // Configuration max_pool_size: AtomicUsize, _min_buffer_size: AtomicU64, _max_buffer_size: AtomicU64, alignment_requirement: AtomicU64, - + // Statistics allocation_count: AtomicU64, deallocation_count: AtomicU64, @@ -104,7 +107,7 @@ pub struct BufferPool { pool_misses: AtomicU64, total_allocated_bytes: AtomicU64, peak_allocated_bytes: AtomicU64, - + // Performance tracking allocation_times: RwLock>, usage_patterns: RwLock>, @@ -124,17 +127,17 @@ pub struct BufferUsageStats { pub struct DynamicBuffer { _device: Arc, buffer_pool: Arc, - + // Current buffer current_buffer: RwLock>>, current_size: AtomicU64, used_size: AtomicU64, - + // Configuration config: BufferConfig, growth_factor: f32, shrink_threshold: f32, - + // Statistics resize_count: AtomicU64, last_resize: RwLock>, @@ -148,7 +151,7 @@ pub struct StagingBuffer { mapped_range: Option>, size: u64, usage_pattern: BufferUsagePattern, - + // Transfer tracking pending_transfers: RwLock>, completed_transfers: AtomicU64, @@ -172,11 +175,11 @@ pub struct RingBuffer { head: AtomicU64, tail: AtomicU64, wrapped: AtomicBool, - + // Configuration alignment: u64, _usage_pattern: BufferUsagePattern, - + // Statistics write_count: AtomicU64, bytes_written: AtomicU64, @@ -187,28 +190,28 @@ pub struct RingBuffer { pub struct BufferManager { device: Arc, _memory_manager: Arc>, - + // Buffer pools vertex_pool: Arc, index_pool: Arc, uniform_pool: Arc, storage_pool: Arc, staging_pool: Arc, - + // Dynamic buffers dynamic_buffers: RwLock>>, - + // Ring buffers ring_buffers: RwLock>>, - + // Staging buffers _staging_buffers: RwLock>>, - + // Global statistics total_buffers: AtomicU64, total_memory_usage: AtomicU64, _fragmentation_ratio: RwLock, - + // Configuration auto_defragmentation: AtomicBool, memory_pressure_threshold: AtomicU64, @@ -232,10 +235,7 @@ impl Default for BufferConfig { impl BufferPool { /// Create a new buffer pool - pub fn new( - device: Arc, - memory_manager: Arc>, - ) -> Self { + pub fn new(device: Arc, memory_manager: Arc>) -> Self { Self { device, _memory_manager: memory_manager, @@ -255,29 +255,29 @@ impl BufferPool { usage_patterns: RwLock::new(HashMap::new()), } } - + /// Allocate a buffer from the pool #[instrument(skip(self))] pub fn allocate(&self, config: &BufferConfig) -> Result { let start_time = Instant::now(); - + // Align size to requirements let aligned_size = self.align_size(config.size); let pool_key = (aligned_size, config.usage); - + // Try to get from pool first if let Some(buffer) = self.try_get_from_pool(&pool_key) { self.pool_hits.fetch_add(1, Ordering::Relaxed); return self.create_allocation(buffer, config, start_time); } - + self.pool_misses.fetch_add(1, Ordering::Relaxed); - + // Create new buffer let buffer = self.create_buffer(config, aligned_size)?; self.create_allocation(Arc::new(buffer), config, start_time) } - + /// Try to get buffer from pool fn try_get_from_pool(&self, pool_key: &(u64, BufferUsages)) -> Option> { let mut pools = self.pools.write(); @@ -287,7 +287,7 @@ impl BufferPool { None } } - + /// Create a new buffer fn create_buffer(&self, config: &BufferConfig, size: u64) -> Result { let buffer = self.device.device.create_buffer(&BufferDescriptor { @@ -296,10 +296,10 @@ impl BufferPool { usage: config.usage, mapped_at_creation: config.mapped_at_creation, }); - + Ok(buffer) } - + /// Create allocation record fn create_allocation( &self, @@ -309,7 +309,7 @@ impl BufferPool { ) -> Result { let handle = ResourceHandle::new(); let allocation_time = start_time.elapsed(); - + let allocation = BufferAllocation { buffer, offset: 0, @@ -320,70 +320,77 @@ impl BufferPool { access_count: AtomicU64::new(0), memory_tier: MemoryTier::HighSpeed, // Default tier }; - + self.active_allocations.write().insert(handle, allocation); - + // Update statistics self.allocation_count.fetch_add(1, Ordering::Relaxed); - self.total_allocated_bytes.fetch_add(config.size, Ordering::Relaxed); - + self.total_allocated_bytes + .fetch_add(config.size, Ordering::Relaxed); + let current_total = self.total_allocated_bytes.load(Ordering::Relaxed); let peak = self.peak_allocated_bytes.load(Ordering::Relaxed); if current_total > peak { - self.peak_allocated_bytes.store(current_total, Ordering::Relaxed); + self.peak_allocated_bytes + .store(current_total, Ordering::Relaxed); } - + // Record allocation time let mut times = self.allocation_times.write(); times.push_back(allocation_time); if times.len() > 1000 { times.pop_front(); } - + // Update usage pattern statistics self.update_usage_stats(config.usage_pattern, config.size); - + debug!( "Allocated buffer '{}' ({} bytes) in {:?}", - config.name, - config.size, - allocation_time + config.name, config.size, allocation_time ); - + Ok(handle) } - + /// Deallocate a buffer #[instrument(skip(self))] pub fn deallocate(&self, handle: ResourceHandle) -> Result<()> { - let allocation = self.active_allocations.write().remove(&handle) + let allocation = self + .active_allocations + .write() + .remove(&handle) .context("Buffer allocation not found")?; - + let pool_key = (allocation.size, allocation.buffer.usage()); - + // Return to pool if there's space let mut pools = self.pools.write(); let pool = pools.entry(pool_key).or_insert_with(VecDeque::new); - + if pool.len() < self.max_pool_size.load(Ordering::Relaxed) { pool.push_back(allocation.buffer); } // Otherwise, buffer will be dropped and freed - + // Update statistics self.deallocation_count.fetch_add(1, Ordering::Relaxed); - self.total_allocated_bytes.fetch_sub(allocation.size, Ordering::Relaxed); - + self.total_allocated_bytes + .fetch_sub(allocation.size, Ordering::Relaxed); + debug!("Deallocated buffer (handle: {:?})", handle); - + Ok(()) } - + /// Get buffer allocation (returns reference to avoid clone issues) pub fn get_allocation(&self, handle: ResourceHandle) -> Option> { - self.active_allocations.read().get(&handle).map(|alloc| alloc.buffer.clone()) + self.active_allocations + .read() + .get(&handle) + .map(|alloc| alloc.buffer.clone()) } - + /// Update access time for buffer pub fn update_access(&self, handle: ResourceHandle) { if let Some(allocation) = self.active_allocations.write().get_mut(&handle) { @@ -391,13 +398,13 @@ impl BufferPool { allocation.access_count.fetch_add(1, Ordering::Relaxed); } } - + /// Align size to requirements fn align_size(&self, size: u64) -> u64 { let alignment = self.alignment_requirement.load(Ordering::Relaxed); (size + alignment - 1) & !(alignment - 1) } - + /// Update usage pattern statistics fn update_usage_stats(&self, pattern: BufferUsagePattern, size: u64) { let mut stats = self.usage_patterns.write(); @@ -405,19 +412,37 @@ impl BufferPool { pattern_stats.allocation_count += 1; pattern_stats.total_size += size; } - + /// Get pool statistics pub fn get_stats(&self) -> HashMap { let mut stats = HashMap::new(); - stats.insert("allocations".to_string(), self.allocation_count.load(Ordering::Relaxed)); - stats.insert("deallocations".to_string(), self.deallocation_count.load(Ordering::Relaxed)); - stats.insert("pool_hits".to_string(), self.pool_hits.load(Ordering::Relaxed)); - stats.insert("pool_misses".to_string(), self.pool_misses.load(Ordering::Relaxed)); - stats.insert("total_bytes".to_string(), self.total_allocated_bytes.load(Ordering::Relaxed)); - stats.insert("peak_bytes".to_string(), self.peak_allocated_bytes.load(Ordering::Relaxed)); + stats.insert( + "allocations".to_string(), + self.allocation_count.load(Ordering::Relaxed), + ); + stats.insert( + "deallocations".to_string(), + self.deallocation_count.load(Ordering::Relaxed), + ); + stats.insert( + "pool_hits".to_string(), + self.pool_hits.load(Ordering::Relaxed), + ); + stats.insert( + "pool_misses".to_string(), + self.pool_misses.load(Ordering::Relaxed), + ); + stats.insert( + "total_bytes".to_string(), + self.total_allocated_bytes.load(Ordering::Relaxed), + ); + stats.insert( + "peak_bytes".to_string(), + self.peak_allocated_bytes.load(Ordering::Relaxed), + ); stats } - + /// Clear unused buffers from pools pub fn cleanup(&self) { let mut pools = self.pools.write(); @@ -426,7 +451,7 @@ impl BufferPool { } info!("Cleared buffer pools"); } - + /// Set maximum pool size pub fn set_max_pool_size(&self, size: usize) { self.max_pool_size.store(size, Ordering::Relaxed); @@ -453,64 +478,62 @@ impl DynamicBuffer { last_resize: RwLock::new(None), } } - + /// Ensure buffer has at least the specified capacity pub fn ensure_capacity(&self, required_size: u64) -> Result<()> { let current_size = self.current_size.load(Ordering::Relaxed); - + if required_size <= current_size { return Ok(()); } - + let new_size = (required_size as f32 * self.growth_factor) as u64; self.resize(new_size)?; - + Ok(()) } - + /// Resize the buffer fn resize(&self, new_size: u64) -> Result<()> { let mut config = self.config.clone(); config.size = new_size; - + let handle = self.buffer_pool.allocate(&config)?; - let allocation = self.buffer_pool.get_allocation(handle) + let allocation = self + .buffer_pool + .get_allocation(handle) .context("Failed to get buffer allocation")?; - + // Copy existing data if needed - if let Some(_old_buffer) = self.current_buffer.read().as_ref() { - - } - + if let Some(_old_buffer) = self.current_buffer.read().as_ref() {} + *self.current_buffer.write() = Some(allocation); self.current_size.store(new_size, Ordering::Relaxed); self.resize_count.fetch_add(1, Ordering::Relaxed); *self.last_resize.write() = Some(Instant::now()); - + debug!("Resized dynamic buffer to {} bytes", new_size); - + Ok(()) } - + /// Get current buffer pub fn get_buffer(&self) -> Option> { self.current_buffer.read().clone() } - + /// Update used size pub fn set_used_size(&self, size: u64) { self.used_size.store(size, Ordering::Relaxed); - + // Check if we should shrink let current_size = self.current_size.load(Ordering::Relaxed); let usage_ratio = size as f32 / current_size as f32; - + if usage_ratio < self.shrink_threshold && current_size > self.config.size { - let new_size = std::cmp::max( - (size as f32 * self.growth_factor) as u64, - self.config.size, - ); - + let new_size = + std::cmp::max((size as f32 * self.growth_factor) as u64, self.config.size); + if let Err(e) = self.resize(new_size) { warn!("Failed to shrink buffer: {}", e); } @@ -532,7 +555,7 @@ impl RingBuffer { usage, mapped_at_creation: false, })); - + Ok(Self { _device: device, buffer, @@ -547,13 +570,13 @@ impl RingBuffer { overruns: AtomicU64::new(0), }) } - + /// Allocate space in the ring buffer pub fn allocate(&self, size: u64) -> Option { let aligned_size = (size + self.alignment - 1) & !(self.alignment - 1); let head = self.head.load(Ordering::Relaxed); let tail = self.tail.load(Ordering::Relaxed); - + let available = if self.wrapped.load(Ordering::Relaxed) { if head >= tail { tail - head @@ -563,37 +586,47 @@ impl RingBuffer { } else { self.size - head }; - + if aligned_size > available { self.overruns.fetch_add(1, Ordering::Relaxed); return None; } - + let offset = head; let new_head = (head + aligned_size) % self.size; - + if new_head < head { self.wrapped.store(true, Ordering::Relaxed); } - + self.head.store(new_head, Ordering::Relaxed); self.write_count.fetch_add(1, Ordering::Relaxed); - self.bytes_written.fetch_add(aligned_size, Ordering::Relaxed); - + self.bytes_written + .fetch_add(aligned_size, Ordering::Relaxed); + Some(offset) } - + /// Get buffer reference pub fn get_buffer(&self) -> &Arc { &self.buffer } - + /// Get statistics pub fn get_stats(&self) -> HashMap { let mut stats = HashMap::new(); - stats.insert("writes".to_string(), self.write_count.load(Ordering::Relaxed)); - stats.insert("bytes_written".to_string(), self.bytes_written.load(Ordering::Relaxed)); - stats.insert("overruns".to_string(), self.overruns.load(Ordering::Relaxed)); + stats.insert( + "writes".to_string(), + self.write_count.load(Ordering::Relaxed), + ); + stats.insert( + "bytes_written".to_string(), + self.bytes_written.load(Ordering::Relaxed), + ); + stats.insert( + "overruns".to_string(), + self.overruns.load(Ordering::Relaxed), + ); stats.insert("head".to_string(), self.head.load(Ordering::Relaxed)); stats.insert("tail".to_string(), self.tail.load(Ordering::Relaxed)); stats @@ -602,16 +635,13 @@ impl RingBuffer { impl BufferManager { /// Create a new buffer manager - pub fn new( - device: Arc, - memory_manager: Arc>, - ) -> Self { + pub fn new(device: Arc, memory_manager: Arc>) -> Self { let vertex_pool = Arc::new(BufferPool::new(device.clone(), memory_manager.clone())); let index_pool = Arc::new(BufferPool::new(device.clone(), memory_manager.clone())); let uniform_pool = Arc::new(BufferPool::new(device.clone(), memory_manager.clone())); let storage_pool = Arc::new(BufferPool::new(device.clone(), memory_manager.clone())); let staging_pool = Arc::new(BufferPool::new(device.clone(), memory_manager.clone())); - + Self { device, _memory_manager: memory_manager, @@ -631,49 +661,46 @@ impl BufferManager { _profiling_enabled: AtomicBool::new(true), } } - + /// Allocate a buffer with the specified configuration pub fn allocate_buffer(&self, config: &BufferConfig) -> Result { let pool = self.get_pool_for_usage(config.usage); let handle = pool.allocate(config)?; - + self.total_buffers.fetch_add(1, Ordering::Relaxed); - self.total_memory_usage.fetch_add(config.size, Ordering::Relaxed); - + self.total_memory_usage + .fetch_add(config.size, Ordering::Relaxed); + Ok(handle) } - + /// Deallocate a buffer pub fn deallocate_buffer(&self, handle: ResourceHandle, usage: BufferUsages) -> Result<()> { let pool = self.get_pool_for_usage(usage); - + if let Some(_buffer) = pool.get_allocation(handle) { // Size tracking would need to be handled differently // For now, just proceed with deallocation } - + pool.deallocate(handle)?; self.total_buffers.fetch_sub(1, Ordering::Relaxed); - + Ok(()) } - + /// Create a dynamic buffer pub fn create_dynamic_buffer(&self, config: BufferConfig) -> Result { let handle = ResourceHandle::new(); let pool = self.get_pool_for_usage(config.usage); - - let dynamic_buffer = Arc::new(DynamicBuffer::new( - self.device.clone(), - pool, - config, - )); - + + let dynamic_buffer = Arc::new(DynamicBuffer::new(self.device.clone(), pool, config)); + self.dynamic_buffers.write().insert(handle, dynamic_buffer); - + Ok(handle) } - + /// Create a ring buffer pub fn create_ring_buffer( &self, @@ -682,21 +709,21 @@ impl BufferManager { usage_pattern: BufferUsagePattern, ) -> Result { let handle = ResourceHandle::new(); - + let ring_buffer = Arc::new(RingBuffer::new( self.device.clone(), size, usage, usage_pattern, )?); - + self.ring_buffers.write().insert(handle, ring_buffer); self.total_buffers.fetch_add(1, Ordering::Relaxed); self.total_memory_usage.fetch_add(size, Ordering::Relaxed); - + Ok(handle) } - + /// Get pool for buffer usage fn get_pool_for_usage(&self, usage: BufferUsages) -> Arc { if usage.contains(BufferUsages::VERTEX) { @@ -711,27 +738,33 @@ impl BufferManager { self.staging_pool.clone() } } - + /// Get buffer manager statistics pub fn get_stats(&self) -> HashMap { let mut stats = HashMap::new(); - stats.insert("total_buffers".to_string(), self.total_buffers.load(Ordering::Relaxed)); - stats.insert("total_memory".to_string(), self.total_memory_usage.load(Ordering::Relaxed)); - + stats.insert( + "total_buffers".to_string(), + self.total_buffers.load(Ordering::Relaxed), + ); + stats.insert( + "total_memory".to_string(), + self.total_memory_usage.load(Ordering::Relaxed), + ); + // Add pool-specific stats let vertex_stats = self.vertex_pool.get_stats(); for (key, value) in vertex_stats { stats.insert(format!("vertex_{}", key), value); } - + let uniform_stats = self.uniform_pool.get_stats(); for (key, value) in uniform_stats { stats.insert(format!("uniform_{}", key), value); } - + stats } - + /// Get buffer by handle pub fn get_buffer(&self, handle: ResourceHandle) -> Option> { // Check all pools @@ -750,17 +783,17 @@ impl BufferManager { if let Some(buffer) = self.staging_pool.get_allocation(handle) { return Some(buffer); } - + // Check dynamic buffers if let Some(dynamic) = self.dynamic_buffers.read().get(&handle) { return dynamic.get_buffer(); } - + // Check ring buffers if let Some(ring) = self.ring_buffers.read().get(&handle) { return Some(ring.get_buffer().clone()); } - + None } @@ -769,49 +802,50 @@ impl BufferManager { if !self.auto_defragmentation.load(Ordering::Relaxed) { return Ok(()); } - + let memory_usage = self.total_memory_usage.load(Ordering::Relaxed); let threshold = self.memory_pressure_threshold.load(Ordering::Relaxed); - + if memory_usage > threshold { info!("Starting buffer defragmentation"); - + // Clear unused buffers from pools self.vertex_pool.cleanup(); self.index_pool.cleanup(); self.uniform_pool.cleanup(); self.storage_pool.cleanup(); self.staging_pool.cleanup(); - + info!("Buffer defragmentation completed"); } - + Ok(()) } - + /// Set auto defragmentation pub fn set_auto_defragmentation(&self, enabled: bool) { self.auto_defragmentation.store(enabled, Ordering::Relaxed); } - + /// Set memory pressure threshold pub fn set_memory_pressure_threshold(&self, threshold: u64) { - self.memory_pressure_threshold.store(threshold, Ordering::Relaxed); + self.memory_pressure_threshold + .store(threshold, Ordering::Relaxed); } - + /// Initialize the buffer manager (integration method) pub fn initialize(&self) -> Result<()> { info!("Buffer manager initialized"); Ok(()) } - + /// Create buffer (integration method) pub fn create_buffer(&self, config: &BufferConfig) -> Result { self.allocate_buffer(config) } - + /// Collect garbage (integration method) pub fn collect_garbage(&self) { let _ = self.defragment(); } -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/device.rs b/crates/strato-renderer/src/device.rs index 976d918..b1962bd 100644 --- a/crates/strato-renderer/src/device.rs +++ b/crates/strato-renderer/src/device.rs @@ -8,19 +8,22 @@ //! - Power management and thermal monitoring //! - Vendor-specific optimizations (NVIDIA, AMD, Intel, Apple) +use anyhow::{bail, Result}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::sync::{Arc, atomic::{AtomicBool, AtomicU64, Ordering}}; +use std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, +}; use std::time::{Duration, Instant}; -use parking_lot::RwLock; +use strato_core::{logging::LogCategory, strato_debug, strato_error_rate_limited, strato_warn}; +use tracing::{debug, info, instrument, warn}; use wgpu::{ - Adapter, Device, DeviceDescriptor, Features, Instance, Limits, Queue, RequestDeviceError, Surface, SurfaceConfiguration, - DeviceType, InstanceDescriptor, Backends, InstanceFlags, Dx12Compiler, Gles3MinorVersion, - PowerPreference, RequestAdapterOptions + Adapter, Backends, Device, DeviceDescriptor, DeviceType, Dx12Compiler, Features, + Gles3MinorVersion, Instance, InstanceDescriptor, InstanceFlags, Limits, PowerPreference, Queue, + RequestAdapterOptions, RequestDeviceError, Surface, SurfaceConfiguration, }; -use anyhow::{Result, bail}; -use tracing::{info, warn, debug, instrument}; -use serde::{Serialize, Deserialize}; -use strato_core::{strato_error_rate_limited, strato_warn, strato_debug, logging::LogCategory}; /// GPU vendor identification #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -100,10 +103,10 @@ impl GpuCapabilities { let info = adapter.get_info(); let limits = adapter.limits(); let features = adapter.features(); - + let vendor = GpuVendor::from(info.vendor); let performance_tier = Self::classify_performance_tier(&info, &limits); - + Self { vendor, device_name: info.name.clone(), @@ -134,11 +137,12 @@ impl GpuCapabilities { limits, } } - + fn classify_performance_tier(info: &AdapterInfo, limits: &Limits) -> PerformanceTier { let memory_score = (limits.max_buffer_size / (1024 * 1024 * 1024)) as u32; // GB - let compute_score = limits.max_compute_workgroup_size_x * limits.max_compute_workgroup_size_y; - + let compute_score = + limits.max_compute_workgroup_size_x * limits.max_compute_workgroup_size_y; + match info.device_type { DeviceType::DiscreteGpu => { if memory_score >= 16 && compute_score >= 1024 * 1024 { @@ -163,7 +167,7 @@ impl GpuCapabilities { DeviceType::Other => PerformanceTier::Entry, } } - + fn estimate_memory_size(info: &AdapterInfo, limits: &Limits) -> u64 { // Rough estimation based on buffer limits and device type match info.device_type { @@ -179,12 +183,12 @@ impl GpuCapabilities { _ => limits.max_buffer_size, } } - + fn estimate_memory_bandwidth(_info: &AdapterInfo, _limits: &Limits) -> Option { // Would need vendor-specific APIs or lookup tables None } - + fn estimate_compute_units(info: &AdapterInfo) -> Option { // Would need vendor-specific detection match GpuVendor::from(info.vendor) { @@ -199,7 +203,7 @@ impl GpuCapabilities { _ => None, } } - + /// Get vendor-specific optimization hints pub fn get_optimization_hints(&self) -> OptimizationHints { match self.vendor { @@ -349,27 +353,32 @@ impl DeviceManager { /// Create a new device manager #[instrument(skip(instance, surface))] pub async fn new(instance: Option, surface: Option<&Surface<'_>>) -> Result { - let instance = instance.unwrap_or_else(|| Instance::new(InstanceDescriptor { - backends: Backends::all(), - flags: InstanceFlags::default(), - dx12_shader_compiler: Dx12Compiler::Fxc, - gles_minor_version: Gles3MinorVersion::Automatic, - })); - + let instance = instance.unwrap_or_else(|| { + Instance::new(InstanceDescriptor { + backends: Backends::all(), + flags: InstanceFlags::default(), + dx12_shader_compiler: Dx12Compiler::Fxc, + gles_minor_version: Gles3MinorVersion::Automatic, + }) + }); + info!("Enumerating GPU adapters..."); let adapters = Self::enumerate_adapters(&instance, surface).await?; - + if adapters.is_empty() { bail!("No compatible GPU adapters found"); } - + info!("Found {} compatible GPU adapter(s)", adapters.len()); for (i, (_, caps)) in adapters.iter().enumerate() { - info!(" [{}] {} ({:?}, {:?})", i, caps.device_name, caps.vendor, caps.performance_tier); + info!( + " [{}] {} ({:?}, {:?})", + i, caps.device_name, caps.vendor, caps.performance_tier + ); } - + let fallback_chain = Self::create_fallback_chain(&adapters); - + Ok(Self { instance, adapters, @@ -381,159 +390,200 @@ impl DeviceManager { monitoring_enabled: AtomicBool::new(true), }) } - + /// Enumerate and analyze all available adapters - async fn enumerate_adapters(instance: &Instance, surface: Option<&Surface<'_>>) -> Result> { + async fn enumerate_adapters( + instance: &Instance, + surface: Option<&Surface<'_>>, + ) -> Result> { let mut adapters = Vec::new(); - + // Try all power preferences to find all adapters for power_pref in [PowerPreference::HighPerformance, PowerPreference::LowPower] { - if let Some(adapter) = instance.request_adapter(&RequestAdapterOptions { - power_preference: power_pref, - compatible_surface: surface, - force_fallback_adapter: false, - }).await { + if let Some(adapter) = instance + .request_adapter(&RequestAdapterOptions { + power_preference: power_pref, + compatible_surface: surface, + force_fallback_adapter: false, + }) + .await + { let capabilities = GpuCapabilities::from_adapter(&adapter); - + // Check if we already have this adapter - if !adapters.iter().any(|(_, caps): &(Adapter, GpuCapabilities)| { - caps.device_id == capabilities.device_id && - caps.vendor_id == capabilities.vendor_id - }) { + if !adapters + .iter() + .any(|(_, caps): &(Adapter, GpuCapabilities)| { + caps.device_id == capabilities.device_id + && caps.vendor_id == capabilities.vendor_id + }) + { adapters.push((adapter, capabilities)); } } } - + // Also try fallback adapter - if let Some(adapter) = instance.request_adapter(&RequestAdapterOptions { - power_preference: PowerPreference::default(), - compatible_surface: surface, - force_fallback_adapter: true, - }).await { + if let Some(adapter) = instance + .request_adapter(&RequestAdapterOptions { + power_preference: PowerPreference::default(), + compatible_surface: surface, + force_fallback_adapter: true, + }) + .await + { let capabilities = GpuCapabilities::from_adapter(&adapter); - + if !adapters.iter().any(|(_, caps)| { - caps.device_id == capabilities.device_id && - caps.vendor_id == capabilities.vendor_id + caps.device_id == capabilities.device_id && caps.vendor_id == capabilities.vendor_id }) { adapters.push((adapter, capabilities)); } } - + Ok(adapters) } - + /// Create fallback chain ordered by preference fn create_fallback_chain(adapters: &[(Adapter, GpuCapabilities)]) -> Vec { let mut indices: Vec = (0..adapters.len()).collect(); - + // Sort by performance tier (descending), then by memory size (descending) indices.sort_by(|&a, &b| { let caps_a = &adapters[a].1; let caps_b = &adapters[b].1; - - caps_b.performance_tier.cmp(&caps_a.performance_tier) + + caps_b + .performance_tier + .cmp(&caps_a.performance_tier) .then(caps_b.memory_size.cmp(&caps_a.memory_size)) }); - + indices } - + /// Initialize device with automatic selection #[instrument] pub async fn initialize_device(&self) -> Result> { let criteria = self.selection_criteria.read().clone(); self.initialize_device_with_criteria(criteria).await } - + /// Initialize device with specific criteria #[instrument] - pub async fn initialize_device_with_criteria(&self, criteria: DeviceSelectionCriteria) -> Result> { + pub async fn initialize_device_with_criteria( + &self, + criteria: DeviceSelectionCriteria, + ) -> Result> { let fallback_chain = self.fallback_chain.read().clone(); - + for &adapter_idx in &fallback_chain { let (adapter, capabilities) = &self.adapters[adapter_idx]; - + if !Self::meets_criteria(capabilities, &criteria) { - debug!("Adapter {} doesn't meet criteria, skipping", capabilities.device_name); + debug!( + "Adapter {} doesn't meet criteria, skipping", + capabilities.device_name + ); continue; } - + match self.create_device(adapter, capabilities, &criteria).await { Ok(device) => { - info!("Successfully initialized device: {}", capabilities.device_name); + info!( + "Successfully initialized device: {}", + capabilities.device_name + ); let managed_device = Arc::new(device); *self.active_device.write() = Some(managed_device.clone()); *self.active_adapter_index.write() = Some(adapter_idx); return Ok(managed_device); } Err(e) => { - warn!("Failed to create device {}: {}", capabilities.device_name, e); + warn!( + "Failed to create device {}: {}", + capabilities.device_name, e + ); continue; } } } - + bail!("Failed to initialize any compatible device"); } - + /// Check if capabilities meet selection criteria fn meets_criteria(capabilities: &GpuCapabilities, criteria: &DeviceSelectionCriteria) -> bool { if capabilities.memory_size < criteria.min_memory_size { return false; } - + if capabilities.performance_tier < criteria.min_performance_tier { return false; } - - if !capabilities.supported_features.contains(criteria.required_features) { + + if !capabilities + .supported_features + .contains(criteria.required_features) + { return false; } - + if let Some(preferred_vendor) = criteria.preferred_vendor { if capabilities.vendor != preferred_vendor { return false; } } - - if criteria.require_timestamp_queries && !capabilities.supported_features.contains(Features::TIMESTAMP_QUERY) { + + if criteria.require_timestamp_queries + && !capabilities + .supported_features + .contains(Features::TIMESTAMP_QUERY) + { return false; } - - if criteria.require_pipeline_statistics && !capabilities.supported_features.contains(Features::PIPELINE_STATISTICS_QUERY) { + + if criteria.require_pipeline_statistics + && !capabilities + .supported_features + .contains(Features::PIPELINE_STATISTICS_QUERY) + { return false; } - + true } - + /// Create managed device from adapter - async fn create_device(&self, adapter: &Adapter, capabilities: &GpuCapabilities, criteria: &DeviceSelectionCriteria) -> Result { + async fn create_device( + &self, + adapter: &Adapter, + capabilities: &GpuCapabilities, + criteria: &DeviceSelectionCriteria, + ) -> Result { info!("Creating device for adapter: {}", capabilities.device_name); - + let mut required_features = criteria.required_features; - + // Enable timestamp queries if requested if criteria.require_timestamp_queries { required_features |= Features::TIMESTAMP_QUERY; } - + // Enable pipeline statistics if requested if criteria.require_pipeline_statistics { required_features |= Features::PIPELINE_STATISTICS_QUERY; } - + let required_limits = Limits::default(); - + // Set up error callback for Vulkan validation errors let device_descriptor = DeviceDescriptor { label: Some(&format!("StratoUI Device - {}", capabilities.device_name)), required_features, required_limits: required_limits.clone(), }; - + match adapter.request_device(&device_descriptor, None).await { Ok((device, queue)) => { // Set up error callback to handle Vulkan validation errors with rate limiting @@ -541,36 +591,50 @@ impl DeviceManager { match error { wgpu::Error::Validation { description, .. } => { // Rate limit Vulkan validation errors, especially VUID-vkQueueSubmit - if description.contains("VUID-vkQueueSubmit") || - description.contains("pSignalSemaphores") { - strato_error_rate_limited!(LogCategory::Vulkan, - "Vulkan validation warning (known WGPU issue): {}", description); + if description.contains("VUID-vkQueueSubmit") + || description.contains("pSignalSemaphores") + { + strato_error_rate_limited!( + LogCategory::Vulkan, + "Vulkan validation warning (known WGPU issue): {}", + description + ); } else { - strato_error_rate_limited!(LogCategory::Vulkan, - "Vulkan validation error: {}", description); + strato_error_rate_limited!( + LogCategory::Vulkan, + "Vulkan validation error: {}", + description + ); } } wgpu::Error::OutOfMemory { .. } => { - strato_error_rate_limited!(LogCategory::Vulkan, - "GPU out of memory: {}", error); + strato_error_rate_limited!( + LogCategory::Vulkan, + "GPU out of memory: {}", + error + ); } _ => { strato_warn!(LogCategory::Vulkan, "GPU error: {}", error); } } })); - + let health = Arc::new(DeviceHealth::default()); - health.last_successful_operation.write().clone_from(&Instant::now()); - + health + .last_successful_operation + .write() + .clone_from(&Instant::now()); + let optimization_hints = capabilities.get_optimization_hints(); - - strato_debug!(LogCategory::Renderer, - "Successfully created device '{}' with {} MB memory", - capabilities.device_name, + + strato_debug!( + LogCategory::Renderer, + "Successfully created device '{}' with {} MB memory", + capabilities.device_name, capabilities.memory_size / (1024 * 1024) ); - + Ok(ManagedDevice { device, queue, @@ -581,17 +645,19 @@ impl DeviceManager { }) } Err(e) => { - strato_error_rate_limited!(LogCategory::Vulkan, - "Failed to create device for adapter '{}': {}", - capabilities.device_name, e + strato_error_rate_limited!( + LogCategory::Vulkan, + "Failed to create device for adapter '{}': {}", + capabilities.device_name, + e ); - + // All device creation errors are treated the same way bail!("Failed to create device: {}", e); } } } - + /// Get current active device pub fn get_device(&self) -> Option> { self.active_device.read().clone() @@ -599,9 +665,11 @@ impl DeviceManager { /// Get current active adapter pub fn get_active_adapter(&self) -> Option<&Adapter> { - self.active_adapter_index.read().map(|idx| &self.adapters[idx].0) + self.active_adapter_index + .read() + .map(|idx| &self.adapters[idx].0) } - + /// Check device health and attempt recovery if needed #[instrument] pub async fn check_device_health(&self) -> Result<()> { @@ -613,38 +681,38 @@ impl DeviceManager { } Ok(()) } - + /// Attempt to recover from device loss async fn recover_device(&self) -> Result<()> { info!("Attempting device recovery..."); - + // Clear current device *self.active_device.write() = None; *self.active_adapter_index.write() = None; - + // Try to reinitialize with same criteria let criteria = self.selection_criteria.read().clone(); self.initialize_device_with_criteria(criteria).await?; - + info!("Device recovery successful"); Ok(()) } - + /// Update selection criteria pub fn update_selection_criteria(&self, criteria: DeviceSelectionCriteria) { *self.selection_criteria.write() = criteria; } - + /// Get adapter capabilities pub fn get_adapter_capabilities(&self) -> Vec { self.adapters.iter().map(|(_, caps)| caps.clone()).collect() } - + /// Get the best available device pub fn get_best_device(&self) -> Option> { self.get_device() } - + /// Get device statistics pub fn get_device_stats(&self) -> Option { self.get_device().map(|device| DeviceStats { @@ -678,20 +746,20 @@ pub struct DeviceStats { #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_device_manager_creation() { let manager = DeviceManager::new(None, None).await; assert!(manager.is_ok()); } - + #[test] fn test_gpu_vendor_detection() { assert_eq!(GpuVendor::from(0x10DE), GpuVendor::Nvidia); assert_eq!(GpuVendor::from(0x1002), GpuVendor::Amd); assert_eq!(GpuVendor::from(0x8086), GpuVendor::Intel); } - + #[test] fn test_optimization_hints() { let caps = GpuCapabilities { @@ -723,9 +791,9 @@ mod tests { supported_features: Features::empty(), limits: Limits::default(), }; - + let hints = caps.get_optimization_hints(); assert_eq!(hints.preferred_workgroup_size, (32, 1, 1)); assert!(hints.prefers_texture_arrays); } -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/font_config.rs b/crates/strato-renderer/src/font_config.rs index 2ceaca9..dcffe74 100644 --- a/crates/strato-renderer/src/font_config.rs +++ b/crates/strato-renderer/src/font_config.rs @@ -1,7 +1,7 @@ +use cosmic_text::{fontdb, FontSystem as CosmicFontSystem}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::Path; -use cosmic_text::{fontdb, FontSystem as CosmicFontSystem}; use sys_locale::get_locale; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -13,32 +13,41 @@ pub struct FontConfig { impl Default for FontConfig { fn default() -> Self { let mut safe_fonts = HashMap::new(); - + // Windows safe fonts - safe_fonts.insert("windows".to_string(), vec![ - "segoeui.ttf".to_string(), - "arial.ttf".to_string(), - "tahoma.ttf".to_string(), - "calibri.ttf".to_string(), - "verdana.ttf".to_string(), - ]); - + safe_fonts.insert( + "windows".to_string(), + vec![ + "segoeui.ttf".to_string(), + "arial.ttf".to_string(), + "tahoma.ttf".to_string(), + "calibri.ttf".to_string(), + "verdana.ttf".to_string(), + ], + ); + // macOS safe fonts - safe_fonts.insert("macos".to_string(), vec![ - "SF-Pro-Display-Regular.otf".to_string(), - "HelveticaNeue.ttc".to_string(), - "Arial.ttf".to_string(), - "Helvetica.ttc".to_string(), - ]); - + safe_fonts.insert( + "macos".to_string(), + vec![ + "SF-Pro-Display-Regular.otf".to_string(), + "HelveticaNeue.ttc".to_string(), + "Arial.ttf".to_string(), + "Helvetica.ttc".to_string(), + ], + ); + // Linux safe fonts - safe_fonts.insert("linux".to_string(), vec![ - "DejaVuSans.ttf".to_string(), - "LiberationSans-Regular.ttf".to_string(), - "NotoSans-Regular.ttf".to_string(), - "Ubuntu-R.ttf".to_string(), - ]); - + safe_fonts.insert( + "linux".to_string(), + vec![ + "DejaVuSans.ttf".to_string(), + "LiberationSans-Regular.ttf".to_string(), + "NotoSans-Regular.ttf".to_string(), + "Ubuntu-R.ttf".to_string(), + ], + ); + Self { safe_fonts, fallback_fonts: vec![ @@ -56,13 +65,13 @@ impl FontConfig { let config: FontConfig = serde_json::from_str(&content)?; Ok(config) } - + pub fn save_to_file>(&self, path: P) -> Result<(), Box> { let content = serde_json::to_string_pretty(self)?; std::fs::write(path, content)?; Ok(()) } - + pub fn get_platform_fonts(&self) -> Vec { let platform = if cfg!(target_os = "windows") { "windows" @@ -71,7 +80,7 @@ impl FontConfig { } else { "linux" }; - + self.safe_fonts .get(platform) .cloned() @@ -81,38 +90,39 @@ impl FontConfig { /// Creates a safe font system that loads only specific fonts to avoid problematic system fonts pub fn create_safe_font_system() -> CosmicFontSystem { - let config = FontConfig::load_from_file("safe_fonts.json") - .unwrap_or_else(|_| { - log::warn!("Could not load safe_fonts.json, using default configuration"); - FontConfig::default() - }); - + let config = FontConfig::load_from_file("safe_fonts.json").unwrap_or_else(|_| { + log::warn!("Could not load safe_fonts.json, using default configuration"); + FontConfig::default() + }); + let mut db = fontdb::Database::new(); - + if cfg!(target_os = "windows") { // Load only safe fonts on Windows to avoid problematic fonts like mstmc.ttf let safe_fonts = config.get_platform_fonts(); let system_fonts_dir = std::env::var("WINDIR") .map(|windir| format!("{}\\Fonts", windir)) .unwrap_or_else(|_| "C:\\Windows\\Fonts".to_string()); - + for font_name in safe_fonts { let font_path = format!("{}\\{}", system_fonts_dir, font_name); if std::path::Path::new(&font_path).exists() { db.load_font_file(&font_path).ok(); } } - + // If no fonts were loaded, fall back to a minimal set if db.faces().count() == 0 { log::warn!("No safe fonts found, loading minimal fallback fonts"); - db.load_font_file(format!("{}\\arial.ttf", system_fonts_dir)).ok(); - db.load_font_file(format!("{}\\tahoma.ttf", system_fonts_dir)).ok(); + db.load_font_file(format!("{}\\arial.ttf", system_fonts_dir)) + .ok(); + db.load_font_file(format!("{}\\tahoma.ttf", system_fonts_dir)) + .ok(); } } else { // On macOS and Linux, load system fonts normally db.load_system_fonts(); - + // Explicitly load emoji font on macOS to ensure it's available if cfg!(target_os = "macos") { let emoji_path = "/System/Library/Fonts/Apple Color Emoji.ttc"; @@ -121,7 +131,7 @@ pub fn create_safe_font_system() -> CosmicFontSystem { } } } - + let locale = get_locale().unwrap_or_else(|| "en-US".to_string()); CosmicFontSystem::new_with_locale_and_db(locale, db) -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/font_system.rs b/crates/strato-renderer/src/font_system.rs index 85e0566..56f15c4 100644 --- a/crates/strato-renderer/src/font_system.rs +++ b/crates/strato-renderer/src/font_system.rs @@ -1,5 +1,5 @@ //! Advanced font management system for StratoUI -//! +//! //! This module provides a comprehensive font management system with: //! - Font registration and identification via FontId //! - Glyph atlas for efficient rendering @@ -7,15 +7,15 @@ //! - High-performance text rendering with caching use cosmic_text::{ - Attrs, Buffer, Family, Metrics, Shaping, SwashCache, Wrap, FontSystem as CosmicFontSystem, + Attrs, Buffer, Family, FontSystem as CosmicFontSystem, Metrics, Shaping, SwashCache, Wrap, }; use dashmap::DashMap; -use strato_core::types::{Color, Point}; -use strato_core::layout::Size; use parking_lot::RwLock; -use std::sync::Arc; use std::collections::HashMap; -use wgpu::{Device, Texture, TextureView, Sampler}; +use std::sync::Arc; +use strato_core::layout::Size; +use strato_core::types::{Color, Point}; +use wgpu::{Device, Sampler, Texture, TextureView}; /// Unique identifier for registered fonts #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -121,7 +121,7 @@ impl GlyphAtlas { }); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("Glyph Atlas Sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, @@ -180,7 +180,7 @@ impl GlyphAtlas { let pos = (self.current_x, self.current_y); self.current_x += width; self.row_height = self.row_height.max(height); - + Some(pos) } } @@ -188,7 +188,7 @@ impl GlyphAtlas { /// Advanced font management system /// Creates a safe font system that loads only specific fonts to avoid problematic system fonts /// This function has been moved to font_config.rs for centralized configuration -/// +/// /// FontSystem implementation with advanced text rendering capabilities pub struct FontSystem { @@ -204,10 +204,10 @@ pub struct FontSystem { impl FontSystem { pub fn new(device: &Device) -> Self { let font_system = crate::font_config::create_safe_font_system(); - + // Register default fonts let mut fonts = HashMap::new(); - + // Default system font with platform-specific fallbacks #[cfg(target_os = "windows")] let default_font_chain = vec![ @@ -217,7 +217,7 @@ impl FontSystem { "Arial".to_string(), "sans-serif".to_string(), ]; - + #[cfg(target_os = "macos")] let default_font_chain = vec![ "SF Pro Display".to_string(), @@ -226,7 +226,7 @@ impl FontSystem { "Arial".to_string(), "sans-serif".to_string(), ]; - + #[cfg(target_os = "linux")] let default_font_chain = vec![ "Ubuntu".to_string(), @@ -236,20 +236,20 @@ impl FontSystem { "Arial".to_string(), "sans-serif".to_string(), ]; - - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - let default_font_chain = vec![ - "Arial".to_string(), - "sans-serif".to_string(), - ]; - fonts.insert(FontId::DEFAULT, FontInfo { - id: FontId::DEFAULT, - family: default_font_chain[0].clone(), - fallback_chain: default_font_chain.clone(), - supports_cjk: false, - supports_emoji: true, - }); + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + let default_font_chain = vec!["Arial".to_string(), "sans-serif".to_string()]; + + fonts.insert( + FontId::DEFAULT, + FontInfo { + id: FontId::DEFAULT, + family: default_font_chain[0].clone(), + fallback_chain: default_font_chain.clone(), + supports_cjk: false, + supports_emoji: true, + }, + ); // System font with CJK support #[cfg(target_os = "windows")] @@ -262,7 +262,7 @@ impl FontSystem { "Arial".to_string(), "sans-serif".to_string(), ]; - + #[cfg(target_os = "macos")] let system_font_chain = vec![ "SF Pro Display".to_string(), @@ -273,7 +273,7 @@ impl FontSystem { "Arial".to_string(), "sans-serif".to_string(), ]; - + #[cfg(target_os = "linux")] let system_font_chain = vec![ "Ubuntu".to_string(), @@ -284,34 +284,37 @@ impl FontSystem { "Arial".to_string(), "sans-serif".to_string(), ]; - - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - let system_font_chain = vec![ - "Arial".to_string(), - "sans-serif".to_string(), - ]; - fonts.insert(FontId::SYSTEM, FontInfo { - id: FontId::SYSTEM, - family: system_font_chain[0].clone(), - fallback_chain: system_font_chain, - supports_cjk: true, - supports_emoji: true, - }); + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + let system_font_chain = vec!["Arial".to_string(), "sans-serif".to_string()]; + + fonts.insert( + FontId::SYSTEM, + FontInfo { + id: FontId::SYSTEM, + family: system_font_chain[0].clone(), + fallback_chain: system_font_chain, + supports_cjk: true, + supports_emoji: true, + }, + ); // Monospace font - fonts.insert(FontId::MONOSPACE, FontInfo { - id: FontId::MONOSPACE, - family: "monospace".to_string(), - fallback_chain: vec![ - "Consolas".to_string(), - "Monaco".to_string(), - "Courier New".to_string(), - "monospace".to_string(), - ], - supports_cjk: false, - supports_emoji: false, - }); + fonts.insert( + FontId::MONOSPACE, + FontInfo { + id: FontId::MONOSPACE, + family: "monospace".to_string(), + fallback_chain: vec![ + "Consolas".to_string(), + "Monaco".to_string(), + "Courier New".to_string(), + "monospace".to_string(), + ], + supports_cjk: false, + supports_emoji: false, + }, + ); Self { font_system: Arc::new(RwLock::new(font_system)), @@ -347,7 +350,9 @@ impl FontSystem { /// Create text attributes for rendering fn create_attrs<'a>(&'a self, style: &'a TextStyle) -> Attrs<'a> { - let font_info = self.fonts.get(&style.font) + let font_info = self + .fonts + .get(&style.font) .unwrap_or_else(|| &self.fonts[&FontId::DEFAULT]); Attrs::new() @@ -363,57 +368,63 @@ impl FontSystem { /// Measure text dimensions pub fn measure_text(&self, text: &str, style: &TextStyle, max_width: Option) -> Size { let mut font_system = self.font_system.write(); - + let metrics = Metrics::new(style.size, style.size * style.line_height); let mut buffer = Buffer::new(&mut font_system, metrics); - + let attrs = self.create_attrs(style); buffer.set_text(&mut font_system, text, attrs, Shaping::Advanced); - + if let Some(width) = max_width { buffer.set_wrap(&mut font_system, Wrap::Word); buffer.set_size(&mut font_system, Some(width), Some(f32::MAX)); } - + buffer.shape_until_scroll(&mut font_system, false); - + let mut max_width: f32 = 0.0; let mut total_height = 0.0; - + for run in buffer.layout_runs() { let line_width = run.glyphs.iter().map(|g| g.w).sum::(); max_width = max_width.max(line_width); total_height += run.line_height; } - + Size::new(max_width, total_height) } /// Render text and return vertex data - pub fn render_text(&self, text: &str, style: &TextStyle, position: Point, max_width: Option) -> Vec { + pub fn render_text( + &self, + text: &str, + style: &TextStyle, + position: Point, + max_width: Option, + ) -> Vec { let mut vertices = Vec::new(); let mut font_system = self.font_system.write(); - + let metrics = Metrics::new(style.size, style.size * style.line_height); let mut buffer = Buffer::new(&mut font_system, metrics); - + let attrs = self.create_attrs(style); buffer.set_text(&mut font_system, text, attrs, Shaping::Advanced); - + if let Some(width) = max_width { buffer.set_wrap(&mut font_system, Wrap::Word); buffer.set_size(&mut font_system, Some(width), Some(f32::MAX)); } - + buffer.shape_until_scroll(&mut font_system, false); - + // Process glyphs and add to atlas if needed for run in buffer.layout_runs() { for glyph in run.glyphs { let physical_glyph = glyph.physical((position.x, position.y), 1.0); // Add glyph to atlas and create vertex let tex_coords = self.ensure_glyph_in_atlas(&physical_glyph); - + vertices.push(TextVertex { position: [physical_glyph.x as f32, physical_glyph.y as f32], color: [style.color.r, style.color.g, style.color.b, style.color.a], @@ -421,7 +432,7 @@ impl FontSystem { }); } } - + vertices } @@ -484,4 +495,4 @@ impl TextRenderer { pub fn glyph_atlas(&self) -> &Arc> { &self.font_system.glyph_atlas } -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/glyph_atlas.rs b/crates/strato-renderer/src/glyph_atlas.rs index 2661178..1721382 100644 --- a/crates/strato-renderer/src/glyph_atlas.rs +++ b/crates/strato-renderer/src/glyph_atlas.rs @@ -4,12 +4,12 @@ //! for efficient GPU rendering. It uses cosmic-text for glyph rasterization and manages //! texture space allocation using a simple bin-packing algorithm. +use crate::font_config::create_safe_font_system; +use crate::text::Font; +use cosmic_text::{CacheKey, FontSystem, SwashCache}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use cosmic_text::{CacheKey, FontSystem, SwashCache}; -use crate::font_config::create_safe_font_system; use strato_core::types::Color; -use crate::text::Font; /// Represents a glyph in the atlas #[derive(Debug, Clone, Copy)] @@ -55,14 +55,21 @@ impl GlyphAtlas { } /// Add a glyph to the atlas - pub fn add_glyph(&mut self, cache_key: CacheKey, glyph_bitmap: &[u8], size: (u32, u32), bearing: (i32, i32), advance: f32) -> Option { + pub fn add_glyph( + &mut self, + cache_key: CacheKey, + glyph_bitmap: &[u8], + size: (u32, u32), + bearing: (i32, i32), + advance: f32, + ) -> Option { let (glyph_width, glyph_height) = size; - + // Check if glyph is already in atlas if let Some(info) = self.glyph_map.get(&cache_key) { return Some(*info); } - + // Check if we have space in current row if self.current_x + glyph_width > self.dimensions.0 { // Move to next row @@ -70,17 +77,17 @@ impl GlyphAtlas { self.current_x = 0; self.current_row_height = 0; } - + // Check if we have vertical space if self.current_row_y + glyph_height > self.dimensions.1 { // Atlas is full return None; } - + // Copy glyph data to atlas let atlas_x = self.current_x; let atlas_y = self.current_row_y; - + for y in 0..glyph_height { for x in 0..glyph_width { let src_idx = (y * glyph_width + x) as usize; @@ -90,26 +97,26 @@ impl GlyphAtlas { } } } - + // Calculate UV coordinates let u_min = atlas_x as f32 / self.dimensions.0 as f32; let v_min = atlas_y as f32 / self.dimensions.1 as f32; let u_max = (atlas_x + glyph_width) as f32 / self.dimensions.0 as f32; let v_max = (atlas_y + glyph_height) as f32 / self.dimensions.1 as f32; - + let glyph_info = GlyphInfo { uv_rect: (u_min, v_min, u_max, v_max), size, bearing, advance, }; - + // Update atlas state self.current_x += glyph_width; self.current_row_height = self.current_row_height.max(glyph_height); self.glyph_map.insert(cache_key, glyph_info); self.dirty = true; - + Some(glyph_info) } @@ -174,10 +181,10 @@ impl GlyphAtlasManager { /// Get or create a glyph in an atlas pub fn get_or_create_glyph( - &mut self, + &mut self, font_system: &mut FontSystem, swash_cache: &mut SwashCache, - cache_key: CacheKey + cache_key: CacheKey, ) -> Option<(usize, GlyphInfo)> { // Check existing atlases first for (atlas_idx, atlas) in self.atlases.iter().enumerate() { @@ -188,41 +195,44 @@ impl GlyphAtlasManager { // Need to rasterize the glyph // Rasterize using swash - let image = swash_cache.get_image(font_system, cache_key).as_ref().cloned()?; - + let image = swash_cache + .get_image(font_system, cache_key) + .as_ref() + .cloned()?; + let glyph_width = image.placement.width; let glyph_height = image.placement.height; let bearing_x = image.placement.left; let bearing_y = image.placement.top; - + // Convert content to alpha mask (if it's not already?) // swash_cache.get_image returns image data. cosmic-text uses Format::Alpha usually? // Let's check image.content. - + let glyph_bitmap = match image.content { cosmic_text::SwashContent::Mask => image.data, cosmic_text::SwashContent::SubpixelMask => { - // Convert subpixel to standard alpha? Or just use it? - // For now assume we handle it as alpha or it's handled by shader - // We'll take every 3rd byte or average? - // For simplicity, let's just take it as is, but it might be 3x wider? - // No, cosmic-text handles this. - image.data + // Convert subpixel to standard alpha? Or just use it? + // For now assume we handle it as alpha or it's handled by shader + // We'll take every 3rd byte or average? + // For simplicity, let's just take it as is, but it might be 3x wider? + // No, cosmic-text handles this. + image.data } cosmic_text::SwashContent::Color => { // Color emoji etc. Not supported in our simple atlas yet (grayscale). - return None; + return None; } }; // Try to add to existing atlases for (atlas_idx, atlas) in self.atlases.iter_mut().enumerate() { if let Some(info) = atlas.add_glyph( - cache_key, - &glyph_bitmap, + cache_key, + &glyph_bitmap, (glyph_width, glyph_height), (bearing_x, bearing_y), - 0.0 // Advance is handled by layout run, we store 0 or don't use it in vertex gen + 0.0, // Advance is handled by layout run, we store 0 or don't use it in vertex gen ) { return Some((atlas_idx, info)); } @@ -231,11 +241,11 @@ impl GlyphAtlasManager { // Create new atlas if needed let mut new_atlas = GlyphAtlas::new(self.atlas_size.0, self.atlas_size.1); if let Some(info) = new_atlas.add_glyph( - cache_key, - &glyph_bitmap, + cache_key, + &glyph_bitmap, (glyph_width, glyph_height), (bearing_x, bearing_y), - 0.0 + 0.0, ) { let atlas_idx = self.atlases.len(); self.atlases.push(new_atlas); @@ -331,7 +341,12 @@ mod tests { let info = info.unwrap(); assert_eq!( info.uv_rect, - (0.0, 0.0, glyph_width as f32 / 256.0, glyph_height as f32 / 256.0) + ( + 0.0, + 0.0, + glyph_width as f32 / 256.0, + glyph_height as f32 / 256.0 + ) ); } diff --git a/crates/strato-renderer/src/gpu/buffer_mgr.rs b/crates/strato-renderer/src/gpu/buffer_mgr.rs index 4aa36f2..6cd526e 100644 --- a/crates/strato-renderer/src/gpu/buffer_mgr.rs +++ b/crates/strato-renderer/src/gpu/buffer_mgr.rs @@ -3,12 +3,12 @@ //! BLOCCO 4: Buffer Management //! Handles GPU buffer creation, upload, and management +use std::mem; use wgpu::{ util::{BufferInitDescriptor, DeviceExt}, Buffer, BufferAddress, BufferUsages, Device, Queue, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode, }; -use std::mem; /// Simple vertex for 2D rendering (position + color + UV) #[repr(C)] @@ -42,19 +42,22 @@ impl SimpleVertex { }, // UV (Location 2) VertexAttribute { - offset: (mem::size_of::<[f32; 2]>() + mem::size_of::<[f32; 4]>()) as BufferAddress, + offset: (mem::size_of::<[f32; 2]>() + mem::size_of::<[f32; 4]>()) + as BufferAddress, shader_location: 2, format: VertexFormat::Float32x2, }, // Params (Location 3) VertexAttribute { - offset: (mem::size_of::<[f32; 2]>() * 2 + mem::size_of::<[f32; 4]>()) as BufferAddress, + offset: (mem::size_of::<[f32; 2]>() * 2 + mem::size_of::<[f32; 4]>()) + as BufferAddress, shader_location: 3, format: VertexFormat::Float32x4, }, // Flags (Location 4) VertexAttribute { - offset: (mem::size_of::<[f32; 2]>() * 2 + mem::size_of::<[f32; 4]>() * 2) as BufferAddress, + offset: (mem::size_of::<[f32; 2]>() * 2 + mem::size_of::<[f32; 4]>() * 2) + as BufferAddress, shader_location: 4, format: VertexFormat::Uint32, }, @@ -84,7 +87,7 @@ impl BufferManager { /// * `device` - GPU device pub fn new(device: &Device) -> Self { // Create initial empty buffers with COPY_DST usage for updates - + let vertex_size = Self::INITIAL_VERTEX_COUNT * mem::size_of::() as u64; let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Vertex Buffer"), @@ -224,8 +227,20 @@ mod tests { let mut buffer_mgr = BufferManager::new(dm.device()); let vertices = vec![ - SimpleVertex { position: [0.0, 0.0], color: [1.0, 0.0, 0.0, 1.0], uv: [0.0, 0.0], params: [0.0; 4], flags: 0 }, - SimpleVertex { position: [1.0, 1.0], color: [0.0, 1.0, 0.0, 1.0], uv: [1.0, 1.0], params: [0.0; 4], flags: 0 }, + SimpleVertex { + position: [0.0, 0.0], + color: [1.0, 0.0, 0.0, 1.0], + uv: [0.0, 0.0], + params: [0.0; 4], + flags: 0, + }, + SimpleVertex { + position: [1.0, 1.0], + color: [0.0, 1.0, 0.0, 1.0], + uv: [1.0, 1.0], + params: [0.0; 4], + flags: 0, + }, ]; let indices: Vec = vec![0, 1, 2]; let projection = [[1.0; 4]; 4]; diff --git a/crates/strato-renderer/src/gpu/drawing.rs b/crates/strato-renderer/src/gpu/drawing.rs index e938d21..1f99845 100644 --- a/crates/strato-renderer/src/gpu/drawing.rs +++ b/crates/strato-renderer/src/gpu/drawing.rs @@ -3,8 +3,6 @@ //! BLOCCO 7: Drawing System //! Final integration: converts RenderBatch to GPU draw calls -use crate::batch::RenderBatch; -use crate::vertex::VertexBuilder; use super::{ buffer_mgr::{BufferManager, SimpleVertex}, device::DeviceManager, @@ -14,8 +12,10 @@ use super::{ surface::SurfaceManager, texture_mgr::TextureManager, }; -use wgpu::{CommandEncoderDescriptor, IndexFormat}; +use crate::batch::RenderBatch; +use crate::vertex::VertexBuilder; use std::sync::Arc; +use wgpu::{CommandEncoderDescriptor, IndexFormat}; use winit::window::Window; /// Complete drawing system @@ -34,16 +34,16 @@ impl DrawingSystem { /// Create new drawing system pub async fn new(window: Arc) -> anyhow::Result { println!("=== DRAWING SYSTEM INITIALIZATION ==="); - + // BLOCCO 1: Device Setup let device_mgr = DeviceManager::new(wgpu::Backends::all()).await?; println!("✅ DeviceManager initialized"); - + // BLOCCO 2: Surface Configuration let target = unsafe { wgpu::SurfaceTargetUnsafe::from_window(&*window)? }; let surface = unsafe { device_mgr.instance().create_surface_unsafe(target)? }; let size = window.inner_size(); - + let surface_mgr = SurfaceManager::new( surface, device_mgr.device(), @@ -52,7 +52,7 @@ impl DrawingSystem { size.height, )?; println!("✅ SurfaceManager initialized"); - + // BLOCCO 3: Shader Compilation let shader_mgr = ShaderManager::from_wgsl( device_mgr.device(), @@ -60,15 +60,15 @@ impl DrawingSystem { Some("Simple Shader"), )?; println!("✅ ShaderManager initialized"); - + // BLOCCO 4: Buffer Management let buffer_mgr = BufferManager::new(device_mgr.device()); println!("✅ BufferManager initialized"); - - // BLOCCO 8: Texture Management + + // BLOCCO 8: Texture Management let texture_mgr = TextureManager::new_with_font(device_mgr.device(), device_mgr.queue()); println!("✅ TextureManager initialized"); - + // BLOCCO 5: Pipeline Creation let pipeline_mgr = PipelineManager::new( device_mgr.device(), @@ -78,13 +78,13 @@ impl DrawingSystem { surface_mgr.format(), )?; println!("✅ PipelineManager initialized"); - + // BLOCCO 6: Render Pass let render_pass_mgr = RenderPassManager::new(); println!("✅ RenderPassManager initialized"); - + println!("===================================="); - + Ok(Self { device_mgr, surface_mgr, @@ -119,10 +119,9 @@ impl DrawingSystem { let mut current_index_start = 0; let mut current_index_count = 0; let mut scissor_stack: Vec<[u32; 4]> = Vec::new(); - - let get_current_scissor = |stack: &[ [u32; 4] ]| -> Option<[u32; 4]> { - stack.last().cloned() - }; + + let get_current_scissor = + |stack: &[[u32; 4]]| -> Option<[u32; 4]> { stack.last().cloned() }; // Note: We ignore batch.vertices here because we regenerate everything from commands // to ensure correct Z-ordering and support interleaved clipping. @@ -130,7 +129,7 @@ impl DrawingSystem { for command in &batch.commands { match command { crate::batch::DrawCommand::PushClip(rect) => { - // Finish current batch if needed + // Finish current batch if needed if current_index_count > 0 { batches.push(GPUDrawBatch { index_start: current_index_start, @@ -156,8 +155,13 @@ impl DrawingSystem { let min_y = y.max(0); let max_x = (x + w).min(surface_w).max(min_x); let max_y = (y + h).min(surface_h).max(min_y); - - let mut new_rect = [min_x as u32, min_y as u32, (max_x - min_x) as u32, (max_y - min_y) as u32]; + + let mut new_rect = [ + min_x as u32, + min_y as u32, + (max_x - min_x) as u32, + (max_y - min_y) as u32, + ]; // Intersect with current scissor if let Some(parent) = scissor_stack.last() { @@ -170,7 +174,7 @@ impl DrawingSystem { let iy = new_rect[1].max(py); let iw = (new_rect[0] + new_rect[2]).min(px + pw).saturating_sub(ix); let ih = (new_rect[1] + new_rect[3]).min(py + ph).saturating_sub(iy); - + new_rect = [ix, iy, iw, ih]; } @@ -189,33 +193,49 @@ impl DrawingSystem { } scissor_stack.pop(); } - crate::batch::DrawCommand::RoundedRect { rect, color, radius, transform } => { + crate::batch::DrawCommand::RoundedRect { + rect, + color, + radius, + transform, + } => { let color_arr = [color.r, color.g, color.b, color.a]; let (v_list, i_list) = VertexBuilder::rounded_rectangle( - rect.x, rect.y, rect.width, rect.height, *radius, color_arr, 8 + rect.x, + rect.y, + rect.width, + rect.height, + *radius, + color_arr, + 8, ); - + let added_count = v_list.len() as u32; let index_count = i_list.len() as u32; - + for v in v_list { - let mut sv = SimpleVertex::from(&v); - // Apply transform - let p = strato_core::types::Point::new(sv.position[0], sv.position[1]); - let transformed = transform.transform_point(p); - sv.position = [transformed.x, transformed.y]; - vertices.push(sv); + let mut sv = SimpleVertex::from(&v); + // Apply transform + let p = strato_core::types::Point::new(sv.position[0], sv.position[1]); + let transformed = transform.transform_point(p); + sv.position = [transformed.x, transformed.y]; + vertices.push(sv); } - + for i in i_list { indices.push((i as u32) + vertex_count); } vertex_count += added_count; current_index_count += index_count; } - crate::batch::DrawCommand::Rect { rect, color, transform, .. } => { + crate::batch::DrawCommand::Rect { + rect, + color, + transform, + .. + } => { let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); - + // Apply transform using strato_core::Transform method let apply_transform = |p: [f32; 2]| -> [f32; 2] { let point = strato_core::types::Point::new(p[0], p[1]); @@ -229,12 +249,20 @@ impl DrawingSystem { let p3 = apply_transform([x, y + h]); let color_arr = [color.r, color.g, color.b, color.a]; - + // Solid color vertices (uv = 0,0) - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p0, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p1, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p2, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p3, color_arr))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p0, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p1, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p2, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p3, color_arr, + ))); indices.push(vertex_count); indices.push(vertex_count + 1); @@ -246,49 +274,62 @@ impl DrawingSystem { vertex_count += 4; current_index_count += 6; } - crate::batch::DrawCommand::Text { text, position, color, font_size, letter_spacing, align } => { + crate::batch::DrawCommand::Text { + text, + position, + color, + font_size, + letter_spacing, + align, + } => { let (mut x, y) = *position; let color_arr = [color.r, color.g, color.b, color.a]; - let font_size_val = *font_size; + let font_size_val = *font_size; let spacing_val = *letter_spacing; - + // Use scale factor for high-resolution text rasterization let scale = self.scale_factor; let physical_font_size = (font_size_val * scale).round() as u32; - + // Handle alignment if *align != strato_core::text::TextAlign::Left { let mut width = 0.0; for ch in text.chars() { - if let Some(glyph) = self.texture_mgr.get_or_cache_glyph(self.device_mgr.queue(), ch, physical_font_size) { - // Scale metrics back to logical coordinates for layout - let advance = glyph.metrics.advance / scale; - width += advance + spacing_val; - } else if ch == ' ' { - width += font_size_val * 0.3 + spacing_val; - } + if let Some(glyph) = self.texture_mgr.get_or_cache_glyph( + self.device_mgr.queue(), + ch, + physical_font_size, + ) { + // Scale metrics back to logical coordinates for layout + let advance = glyph.metrics.advance / scale; + width += advance + spacing_val; + } else if ch == ' ' { + width += font_size_val * 0.3 + spacing_val; + } } - + match align { strato_core::text::TextAlign::Center => x -= width / 2.0, strato_core::text::TextAlign::Right => x -= width, _ => {} // Justify not implemented yet } } - - let ascent = if let Some(metrics) = self.texture_mgr.get_line_metrics(physical_font_size as f32) { + + let ascent = if let Some(metrics) = + self.texture_mgr.get_line_metrics(physical_font_size as f32) + { metrics.ascent / scale } else { font_size_val * 0.8 // Fallback approximation }; - + let baseline = y + ascent; for ch in text.chars() { if let Some(glyph) = self.texture_mgr.get_or_cache_glyph( self.device_mgr.queue(), - ch, - physical_font_size + ch, + physical_font_size, ) { // Scale metrics back to logical coordinates for rendering let bearing_x = glyph.metrics.bearing_x as f32 / scale; @@ -296,31 +337,31 @@ impl DrawingSystem { let w = glyph.metrics.width as f32 / scale; let h = glyph.metrics.height as f32 / scale; let advance = glyph.metrics.advance / scale; - + let glyph_x = (x + bearing_x).round(); let glyph_y = (baseline - bearing_y).round(); - + let (u0, v0, u1, v1) = glyph.uv_rect; vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [glyph_x, glyph_y], + [glyph_x, glyph_y], [u0, v0], - color_arr + color_arr, ))); vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [glyph_x + w, glyph_y], + [glyph_x + w, glyph_y], [u1, v0], - color_arr + color_arr, ))); vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [glyph_x + w, glyph_y + h], + [glyph_x + w, glyph_y + h], [u1, v1], - color_arr + color_arr, ))); vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [glyph_x, glyph_y + h], + [glyph_x, glyph_y + h], [u0, v1], - color_arr + color_arr, ))); indices.push(vertex_count); @@ -332,7 +373,7 @@ impl DrawingSystem { vertex_count += 4; current_index_count += 6; - + x += advance + spacing_val; } else { if ch == ' ' { @@ -341,37 +382,44 @@ impl DrawingSystem { } } } - crate::batch::DrawCommand::Image { id, data, width, height, rect, color } => { + crate::batch::DrawCommand::Image { + id, + data, + width, + height, + rect, + color, + } => { if let Some(image) = self.texture_mgr.get_or_upload_image( self.device_mgr.queue(), *id, data, *width, - *height + *height, ) { let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); let (u0, v0, u1, v1) = image.uv_rect; let color_arr = [color.r, color.g, color.b, color.a]; vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [x, y], + [x, y], [u0, v0], - color_arr + color_arr, ))); vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [x + w, y], + [x + w, y], [u1, v0], - color_arr + color_arr, ))); vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [x + w, y + h], + [x + w, y + h], [u1, v1], - color_arr + color_arr, ))); vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( - [x, y + h], + [x, y + h], [u0, v1], - color_arr + color_arr, ))); indices.push(vertex_count); @@ -385,7 +433,14 @@ impl DrawingSystem { current_index_count += 6; } } - crate::batch::DrawCommand::TexturedQuad { rect, texture_id: _, uv_rect, color, transform, .. } => { + crate::batch::DrawCommand::TexturedQuad { + rect, + texture_id: _, + uv_rect, + color, + transform, + .. + } => { let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height); let (u, v, uw, vh) = (uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height); let color_arr = [color.r, color.g, color.b, color.a]; @@ -406,10 +461,18 @@ impl DrawingSystem { let uv2 = [u + uw, v + vh]; let uv3 = [u, v + vh]; - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(p0, uv0, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(p1, uv1, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(p2, uv2, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured(p3, uv3, color_arr))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( + p0, uv0, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( + p1, uv1, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( + p2, uv2, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::textured( + p3, uv3, color_arr, + ))); indices.push(vertex_count); indices.push(vertex_count + 1); @@ -421,12 +484,18 @@ impl DrawingSystem { vertex_count += 4; current_index_count += 6; } - crate::batch::DrawCommand::Circle { center, radius, color, segments, .. } => { + crate::batch::DrawCommand::Circle { + center, + radius, + color, + segments, + .. + } => { let (cx, cy) = *center; let radius = *radius; let color_arr = [color.r, color.g, color.b, color.a]; let segments = *segments; - + // Center vertex vertices.push(SimpleVertex::from(&crate::vertex::Vertex { position: [cx, cy], @@ -435,15 +504,15 @@ impl DrawingSystem { params: [0.0, 0.0, 0.0, 0.0], flags: 0, })); - + let center_index = vertex_count; vertex_count += 1; - + for i in 0..=segments { let angle = (i as f32 / segments as f32) * 2.0 * std::f32::consts::PI; let x = cx + radius * angle.cos(); let y = cy + radius * angle.sin(); - + vertices.push(SimpleVertex::from(&crate::vertex::Vertex { position: [x, y], uv: [0.5 + 0.5 * angle.cos(), 0.5 + 0.5 * angle.sin()], @@ -451,55 +520,69 @@ impl DrawingSystem { params: [0.0, 0.0, 0.0, 0.0], flags: 0, })); - + if i > 0 { indices.push(center_index); indices.push(vertex_count - 1); indices.push(vertex_count); current_index_count += 3; } - + vertex_count += 1; } } - crate::batch::DrawCommand::Line { start, end, color, thickness, .. } => { + crate::batch::DrawCommand::Line { + start, + end, + color, + thickness, + .. + } => { let (x1, y1) = *start; let (x2, y2) = *end; let thickness = *thickness; let color_arr = [color.r, color.g, color.b, color.a]; - + let dx = x2 - x1; let dy = y2 - y1; let length = (dx * dx + dy * dy).sqrt(); - + if length > 0.0 { let nx = -dy / length * thickness * 0.5; let ny = dx / length * thickness * 0.5; - + let p0 = [x1 + nx, y1 + ny]; let p1 = [x2 + nx, y2 + ny]; let p2 = [x2 - nx, y2 - ny]; let p3 = [x1 - nx, y1 - ny]; - - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p0, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p1, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p2, color_arr))); - vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid(p3, color_arr))); - + + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p0, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p1, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p2, color_arr, + ))); + vertices.push(SimpleVertex::from(&crate::vertex::Vertex::solid( + p3, color_arr, + ))); + indices.push(vertex_count); indices.push(vertex_count + 1); indices.push(vertex_count + 2); indices.push(vertex_count); indices.push(vertex_count + 2); indices.push(vertex_count + 3); - + vertex_count += 4; current_index_count += 6; } } } } - + // Push final batch if current_index_count > 0 { batches.push(GPUDrawBatch { @@ -508,102 +591,115 @@ impl DrawingSystem { scissor: get_current_scissor(&scissor_stack), }); } - + // 3. Upload vertices and indices to GPU self.buffer_mgr.upload_vertices( self.device_mgr.device(), self.device_mgr.queue(), &vertices, ); - self.buffer_mgr.upload_indices( - self.device_mgr.device(), - self.device_mgr.queue(), - &indices, - ); - + self.buffer_mgr + .upload_indices(self.device_mgr.device(), self.device_mgr.queue(), &indices); + // 4. Upload projection matrix (orthographic for 2D) // Use logical size for projection to handle DPI scaling correctly let width = self.surface_mgr.width() as f32; let height = self.surface_mgr.height() as f32; - + // Adjust projection for DPI scale factor // If scale_factor is 2.0 (Retina), physical width is 2x logical width. // We want to use logical coordinates (e.g. 0..400) which map to physical pixels (0..800). // So we project 0..width/scale to -1..1. let logical_width = width / self.scale_factor; let logical_height = height / self.scale_factor; - + let projection = create_orthographic_projection(logical_width, logical_height); - self.buffer_mgr.upload_projection(self.device_mgr.queue(), &projection); - + self.buffer_mgr + .upload_projection(self.device_mgr.queue(), &projection); + // 5. Get surface texture let surface_texture = self.surface_mgr.get_current_texture()?; let view = surface_texture .texture .create_view(&wgpu::TextureViewDescriptor::default()); - + // 6. Create command encoder - let mut encoder = self - .device_mgr - .device() - .create_command_encoder(&CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); - + let mut encoder = + self.device_mgr + .device() + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + // 7. Begin render pass { let mut render_pass = self.render_pass_mgr.begin(&mut encoder, &view); - + // 8. Set pipeline and bind groups render_pass.set_pipeline(self.pipeline_mgr.pipeline()); render_pass.set_bind_group(0, self.pipeline_mgr.bind_group(), &[]); - + // 9. Set vertex/index buffers render_pass.set_vertex_buffer(0, self.buffer_mgr.vertex_buffer().slice(..)); render_pass.set_index_buffer( self.buffer_mgr.index_buffer().slice(..), IndexFormat::Uint32, ); - + // 10. Draw indexed for batch in batches { - if batch.index_count == 0 { continue; } - - // Apply scissor - if let Some(scissor) = batch.scissor { - if scissor[2] == 0 || scissor[3] == 0 { - continue; - } - render_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); - } else { - render_pass.set_scissor_rect(0, 0, self.surface_mgr.width(), self.surface_mgr.height()); - } - - render_pass.draw_indexed(batch.index_start .. batch.index_start + batch.index_count, 0, 0..1); + if batch.index_count == 0 { + continue; + } + + // Apply scissor + if let Some(scissor) = batch.scissor { + if scissor[2] == 0 || scissor[3] == 0 { + continue; + } + render_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]); + } else { + render_pass.set_scissor_rect( + 0, + 0, + self.surface_mgr.width(), + self.surface_mgr.height(), + ); + } + + render_pass.draw_indexed( + batch.index_start..batch.index_start + batch.index_count, + 0, + 0..1, + ); } } - + // 11. Submit command buffer - self.device_mgr.queue().submit(std::iter::once(encoder.finish())); - + self.device_mgr + .queue() + .submit(std::iter::once(encoder.finish())); + // 12. Present surface surface_texture.present(); - + Ok(()) } - + /// Resize surface pub fn resize(&mut self, width: u32, height: u32) -> anyhow::Result<()> { - self.surface_mgr.resize(width, height, self.device_mgr.device())?; - + self.surface_mgr + .resize(width, height, self.device_mgr.device())?; + // Update projection matrix // Use logical size for projection to match render() behavior let logical_width = (width as f32) / self.scale_factor; let logical_height = (height as f32) / self.scale_factor; - + let projection = create_orthographic_projection(logical_width, logical_height); - self.buffer_mgr.upload_projection(self.device_mgr.queue(), &projection); - + self.buffer_mgr + .upload_projection(self.device_mgr.queue(), &projection); + Ok(()) } } @@ -616,7 +712,7 @@ fn create_orthographic_projection(width: f32, height: f32) -> [[f32; 4]; 4] { let right = width; let bottom = height; let top = 0.0; - + [ [2.0 / (right - left), 0.0, 0.0, 0.0], [0.0, 2.0 / (top - bottom), 0.0, 0.0], @@ -636,7 +732,7 @@ impl From<&crate::vertex::Vertex> for SimpleVertex { Self { position: v.position, color: v.color, - uv: v.uv, // Use UV from existing Vertex struct + uv: v.uv, // Use UV from existing Vertex struct params: v.params, flags: v.flags, } @@ -646,24 +742,24 @@ impl From<&crate::vertex::Vertex> for SimpleVertex { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_vertex_conversion() { let vertex = crate::vertex::Vertex::solid([100.0, 200.0], [1.0, 0.0, 0.0, 1.0]); let simple: SimpleVertex = (&vertex).into(); - + assert_eq!(simple.position, [100.0, 200.0]); assert_eq!(simple.color, [1.0, 0.0, 0.0, 1.0]); assert_eq!(simple.uv, vertex.uv); } - + #[test] fn test_orthographic_projection() { let proj = create_orthographic_projection(800.0, 600.0); - + // Top-left corner (0, 0) should map to NDC (-1, 1) // Bottom-right (800, 600) should map to NDC (1, -1) - + // Check matrix is not identity assert_ne!(proj[0][0], 1.0); assert_ne!(proj[1][1], 1.0); diff --git a/crates/strato-renderer/src/gpu/mod.rs b/crates/strato-renderer/src/gpu/mod.rs index 6b93bf1..342748d 100644 --- a/crates/strato-renderer/src/gpu/mod.rs +++ b/crates/strato-renderer/src/gpu/mod.rs @@ -27,11 +27,11 @@ pub mod drawing; pub mod texture_mgr; // Re-exports -pub use device::DeviceManager; -pub use surface::SurfaceManager; -pub use shader_mgr::ShaderManager; pub use buffer_mgr::{BufferManager, SimpleVertex}; +pub use device::DeviceManager; +pub use drawing::DrawingSystem; pub use pipeline_mgr::PipelineManager; pub use render_pass_mgr::RenderPassManager; -pub use drawing::DrawingSystem; -pub use texture_mgr::{TextureManager, TextureAtlas}; +pub use shader_mgr::ShaderManager; +pub use surface::SurfaceManager; +pub use texture_mgr::{TextureAtlas, TextureManager}; diff --git a/crates/strato-renderer/src/gpu/pipeline_mgr.rs b/crates/strato-renderer/src/gpu/pipeline_mgr.rs index c800374..ec18333 100644 --- a/crates/strato-renderer/src/gpu/pipeline_mgr.rs +++ b/crates/strato-renderer/src/gpu/pipeline_mgr.rs @@ -6,12 +6,16 @@ use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState, BufferBindingType, ColorTargetState, - ColorWrites, Device, Face, FragmentState, FrontFace, MultisampleState, PipelineLayoutDescriptor, - PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipeline, RenderPipelineDescriptor, - ShaderStages, TextureFormat, VertexState, + ColorWrites, Device, Face, FragmentState, FrontFace, MultisampleState, + PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipeline, + RenderPipelineDescriptor, ShaderStages, TextureFormat, VertexState, }; -use super::{buffer_mgr::{BufferManager, SimpleVertex}, shader_mgr::ShaderManager, texture_mgr::TextureManager}; +use super::{ + buffer_mgr::{BufferManager, SimpleVertex}, + shader_mgr::ShaderManager, + texture_mgr::TextureManager, +}; /// Manages render pipeline and bind groups pub struct PipelineManager { @@ -37,7 +41,7 @@ impl PipelineManager { surface_format: TextureFormat, ) -> anyhow::Result { println!("=== PIPELINE CREATION ==="); - + // Create bind group layout for uniform buffer + texture + sampler let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { label: Some("Bind Group Layout"), @@ -171,10 +175,10 @@ impl PipelineManager { mod tests { use super::*; use crate::gpu::{DeviceManager, SurfaceManager, TextureManager}; + use std::sync::Arc; use wgpu::Backends; use winit::dpi::PhysicalSize; use winit::event_loop::EventLoop; - use std::sync::Arc; #[tokio::test] async fn test_pipeline_creation() { @@ -191,7 +195,8 @@ mod tests { // Use a common format for testing let format = TextureFormat::Bgra8UnormSrgb; - let pipeline_mgr = PipelineManager::new(dm.device(), &shader, &buffer_mgr, &texture_mgr, format); + let pipeline_mgr = + PipelineManager::new(dm.device(), &shader, &buffer_mgr, &texture_mgr, format); assert!(pipeline_mgr.is_ok()); } @@ -209,7 +214,8 @@ mod tests { let texture_mgr = TextureManager::new(dm.device(), dm.queue()); let format = TextureFormat::Bgra8UnormSrgb; - let pipeline_mgr = PipelineManager::new(dm.device(), &shader, &buffer_mgr, &texture_mgr, format).unwrap(); + let pipeline_mgr = + PipelineManager::new(dm.device(), &shader, &buffer_mgr, &texture_mgr, format).unwrap(); // Verify bind group exists let _bg = pipeline_mgr.bind_group(); @@ -220,7 +226,7 @@ mod tests { fn test_blend_state_configuration() { // Verify blend state is correct (ALPHA_BLENDING) let blend = BlendState::ALPHA_BLENDING; - + // ALPHA_BLENDING should have: // - color: src_alpha * src + (1 - src_alpha) * dst // - alpha: 1 * src + (1 - src_alpha) * dst diff --git a/crates/strato-renderer/src/gpu/render_pass_mgr.rs b/crates/strato-renderer/src/gpu/render_pass_mgr.rs index 37313b6..f8a3147 100644 --- a/crates/strato-renderer/src/gpu/render_pass_mgr.rs +++ b/crates/strato-renderer/src/gpu/render_pass_mgr.rs @@ -45,7 +45,7 @@ impl RenderPassManager { // - Color attachment with clear color // - LoadOp::Clear, StoreOp::Store // - No depth/stencil - + encoder.begin_render_pass(&RenderPassDescriptor { label: Some("Main Render Pass"), color_attachments: &[Some(RenderPassColorAttachment { @@ -76,7 +76,7 @@ mod tests { #[test] fn test_default_clear_color() { let render_pass_mgr = RenderPassManager::new(); - + // Default dark gray assert_eq!(render_pass_mgr.clear_color.r, 0.23); assert_eq!(render_pass_mgr.clear_color.g, 0.23); @@ -87,16 +87,16 @@ mod tests { #[test] fn test_set_clear_color() { let mut render_pass_mgr = RenderPassManager::new(); - + let new_color = wgpu::Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, }; - + render_pass_mgr.set_clear_color(new_color); - + assert_eq!(render_pass_mgr.clear_color.r, 1.0); assert_eq!(render_pass_mgr.clear_color.g, 0.0); } diff --git a/crates/strato-renderer/src/gpu/shader_mgr.rs b/crates/strato-renderer/src/gpu/shader_mgr.rs index 548216a..6d10942 100644 --- a/crates/strato-renderer/src/gpu/shader_mgr.rs +++ b/crates/strato-renderer/src/gpu/shader_mgr.rs @@ -22,11 +22,7 @@ impl ShaderManager { /// /// # Errors /// Returns error if shader compilation fails - pub fn from_wgsl( - device: &Device, - source: &str, - label: Option<&str>, - ) -> anyhow::Result { + pub fn from_wgsl(device: &Device, source: &str, label: Option<&str>) -> anyhow::Result { println!("=== SHADER COMPILATION ==="); println!("Label: {:?}", label); println!("Source length: {} bytes", source.len()); @@ -106,7 +102,7 @@ mod tests { // This test ensures we get expected behavior (panic on invalid WGSL) let invalid_source = "this is not valid WGSL!!!"; let _result = ShaderManager::from_wgsl(dm.device(), invalid_source, Some("Invalid")); - + // Should panic before reaching here } @@ -124,4 +120,3 @@ mod tests { assert_eq!(shader.fragment_entry(), "fs_main"); } } - diff --git a/crates/strato-renderer/src/gpu/surface.rs b/crates/strato-renderer/src/gpu/surface.rs index d616fde..f0bdfdc 100644 --- a/crates/strato-renderer/src/gpu/surface.rs +++ b/crates/strato-renderer/src/gpu/surface.rs @@ -156,7 +156,7 @@ mod tests { let surface = dm.instance().create_surface(window.clone()).unwrap(); // Safety: for test purposes let surface: Surface<'static> = unsafe { std::mem::transmute(surface) }; - + let surface_mgr = SurfaceManager::new(surface, dm.device(), dm.adapter(), 800, 600); assert!(surface_mgr.is_ok()); @@ -178,8 +178,9 @@ mod tests { let surface = dm.instance().create_surface(window.clone()).unwrap(); // Safety: for test purposes let surface: Surface<'static> = unsafe { std::mem::transmute(surface) }; - - let surface_mgr = SurfaceManager::new(surface, dm.device(), dm.adapter(), 800, 600).unwrap(); + + let surface_mgr = + SurfaceManager::new(surface, dm.device(), dm.adapter(), 800, 600).unwrap(); let format = surface_mgr.format(); assert!(matches!( diff --git a/crates/strato-renderer/src/gpu/texture_mgr.rs b/crates/strato-renderer/src/gpu/texture_mgr.rs index 483f337..d4ce833 100644 --- a/crates/strato-renderer/src/gpu/texture_mgr.rs +++ b/crates/strato-renderer/src/gpu/texture_mgr.rs @@ -3,12 +3,12 @@ //! BLOCCO 8: Texture Management //! Handles texture atlas creation, glyph caching, and texture binding -use wgpu::{ - Device, Queue, Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - TextureView, Sampler, SamplerDescriptor, AddressMode, FilterMode, Extent3d, -}; use anyhow::Result; use std::collections::HashMap; +use wgpu::{ + AddressMode, Device, Extent3d, FilterMode, Queue, Sampler, SamplerDescriptor, Texture, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, +}; /// Glyph metrics for positioning and layout #[derive(Debug, Clone, Copy)] @@ -24,14 +24,14 @@ pub struct GlyphMetrics { #[derive(Debug, Clone)] pub struct CachedGlyph { pub metrics: GlyphMetrics, - pub uv_rect: (f32, f32, f32, f32), // (u0, v0, u1, v1) + pub uv_rect: (f32, f32, f32, f32), // (u0, v0, u1, v1) pub atlas_region: (u32, u32, u32, u32), // (x, y, w, h) } /// Cached image with atlas location and UV coordinates #[derive(Debug, Clone)] pub struct CachedImage { - pub uv_rect: (f32, f32, f32, f32), // (u0, v0, u1, v1) + pub uv_rect: (f32, f32, f32, f32), // (u0, v0, u1, v1) pub atlas_region: (u32, u32, u32, u32), // (x, y, w, h) } @@ -81,14 +81,12 @@ impl GlyphRasterizer { pub fn new() -> Result { // Embed Segoe UI Italic font (path from crates/strato-renderer/src/gpu/ to root/font/) const FONT_DATA: &[u8] = include_bytes!("../../../../font/segoeuithis.ttf"); - - let font = fontdue::Font::from_bytes( - FONT_DATA, - fontdue::FontSettings::default() - ).map_err(|e| anyhow::anyhow!("Failed to load font: {}", e))?; + + let font = fontdue::Font::from_bytes(FONT_DATA, fontdue::FontSettings::default()) + .map_err(|e| anyhow::anyhow!("Failed to load font: {}", e))?; println!("=== GLYPH RASTERIZER INITIALIZED ==="); - + Ok(Self { font }) } @@ -261,7 +259,7 @@ impl TextureAtlas { let white_pixel = [255u8, 255, 255, 255]; self.upload_region(queue, &white_pixel, 0, 0, 1, 1) .expect("Failed to upload white pixel"); - + // Advance allocator to skip this pixel // We'll just advance by a small amount to keep it simple, e.g., move to x=1 // effectively reserving the first pixel of the first row @@ -315,11 +313,11 @@ impl TextureManager { pub fn new_with_font(device: &Device, queue: &Queue) -> Self { // Increase atlas size to 2048x2048 to support images let mut atlas = TextureAtlas::new(device, 2048, 2048); - + // IMPORTANT: Reserve white pixel at (0,0) for solid color rendering // The shader samples (0,0) when rendering non-textured shapes atlas.reserve_white_pixel(queue); - + Self { atlas, glyph_cache: GlyphCache::new(), @@ -335,7 +333,10 @@ impl TextureManager { character: char, font_size: u32, ) -> Option<&CachedGlyph> { - let key = GlyphKey { character, font_size }; + let key = GlyphKey { + character, + font_size, + }; // Check cache first if self.glyph_cache.get(&key).is_some() { @@ -347,14 +348,11 @@ impl TextureManager { // Allocate space in atlas if let Some((x, y)) = self.atlas.allocate_region(metrics.width, metrics.height) { // Upload to GPU - if self.atlas.upload_region( - queue, - &rgba_data, - x, - y, - metrics.width, - metrics.height, - ).is_ok() { + if self + .atlas + .upload_region(queue, &rgba_data, x, y, metrics.width, metrics.height) + .is_ok() + { // Calculate UV coordinates let atlas_size = self.atlas.size(); let u0 = x as f32 / atlas_size.0 as f32; @@ -394,14 +392,11 @@ impl TextureManager { // Allocate space in atlas if let Some((x, y)) = self.atlas.allocate_region(width, height) { // Upload to GPU - if self.atlas.upload_region( - queue, - data, - x, - y, - width, - height, - ).is_ok() { + if self + .atlas + .upload_region(queue, data, x, y, width, height) + .is_ok() + { // Calculate UV coordinates let atlas_size = self.atlas.size(); let u0 = x as f32 / atlas_size.0 as f32; @@ -420,7 +415,10 @@ impl TextureManager { println!("Failed to upload image region"); } } else { - println!("Failed to allocate atlas region for image: {}x{}", width, height); + println!( + "Failed to allocate atlas region for image: {}x{}", + width, height + ); } None @@ -449,9 +447,18 @@ mod tests { #[test] fn test_glyph_key() { - let key1 = GlyphKey { character: 'A', font_size: 24 }; - let key2 = GlyphKey { character: 'A', font_size: 24 }; - let key3 = GlyphKey { character: 'B', font_size: 24 }; + let key1 = GlyphKey { + character: 'A', + font_size: 24, + }; + let key2 = GlyphKey { + character: 'A', + font_size: 24, + }; + let key3 = GlyphKey { + character: 'B', + font_size: 24, + }; assert_eq!(key1, key2); assert_ne!(key1, key3); @@ -460,8 +467,11 @@ mod tests { #[test] fn test_glyph_cache() { let mut cache = GlyphCache::new(); - - let key = GlyphKey { character: 'A', font_size: 24 }; + + let key = GlyphKey { + character: 'A', + font_size: 24, + }; let glyph = CachedGlyph { metrics: GlyphMetrics { width: 10, @@ -482,7 +492,7 @@ mod tests { #[test] fn test_glyph_rasterizer() { let rasterizer = GlyphRasterizer::new().unwrap(); - + let result = rasterizer.rasterize('A', 24.0); assert!(result.is_some()); @@ -510,7 +520,7 @@ mod tests { // Test allocation that should succeed (fits in atlas) let region3 = atlas.allocate_region(100, 20); assert!(region3.is_some()); - + // Test allocation that's too wide for the atlas let region_fail = atlas.allocate_region(300, 20); assert_eq!(region_fail, None); @@ -537,7 +547,7 @@ mod tests { async fn test_texture_atlas_creation() { let dm = DeviceManager::new(Backends::all()).await.unwrap(); let atlas = TextureAtlas::new(dm.device(), 256, 256); - + assert_eq!(atlas.size(), (256, 256)); } @@ -545,7 +555,7 @@ mod tests { async fn test_default_white_texture() { let dm = DeviceManager::new(Backends::all()).await.unwrap(); let atlas = TextureAtlas::create_default_white(dm.device(), dm.queue()); - + assert_eq!(atlas.size(), (1, 1)); } } diff --git a/crates/strato-renderer/src/integration.rs b/crates/strato-renderer/src/integration.rs index 8a431a0..060a3f7 100644 --- a/crates/strato-renderer/src/integration.rs +++ b/crates/strato-renderer/src/integration.rs @@ -12,20 +12,20 @@ //! The integration layer ensures all systems work together seamlessly and provides //! a clean, easy-to-use API for the rest of the framework. +use anyhow::{Context, Result}; +use slotmap::DefaultKey; use std::sync::Arc; -use anyhow::{Result, Context}; -use tracing::{info, warn, debug, instrument}; +use tracing::{debug, info, instrument, warn}; use wgpu::*; -use slotmap::DefaultKey; use crate::{ - device::{ManagedDevice, DeviceManager}, - resources::{ResourceManager, ResourceHandle}, - memory::{MemoryManager, AllocationStrategy}, - shader::{ShaderManager, CompiledShader}, - buffer::{BufferManager}, - profiler::{Profiler, PerformanceReport}, - pipeline::{PipelineManager}, + buffer::BufferManager, + device::{DeviceManager, ManagedDevice}, + memory::{AllocationStrategy, MemoryManager}, + pipeline::PipelineManager, + profiler::{PerformanceReport, Profiler}, + resources::{ResourceHandle, ResourceManager}, + shader::{CompiledShader, ShaderManager}, }; /// Configuration for the integrated renderer system @@ -72,20 +72,20 @@ pub struct IntegratedRenderer { // Core systems device_manager: Arc, device: Arc, - + // Management systems resource_manager: Arc, memory_manager: Arc>, shader_manager: Arc, buffer_manager: Arc, pipeline_manager: Arc, - + // Monitoring profiler: Option>, - + // Configuration config: RendererConfig, - + // State initialized: bool, frame_count: u64, @@ -97,7 +97,7 @@ pub struct RenderContext { pub encoder: CommandEncoder, pub profiler: Option>, pub frame_id: u64, - + // Timing queries gpu_timer_id: Option, } @@ -118,24 +118,30 @@ impl IntegratedRenderer { pub async fn new() -> Result { Self::with_config(RendererConfig::default(), None, None).await } - + /// Create a new integrated renderer with custom configuration - pub async fn with_config(config: RendererConfig, instance: Option, surface: Option<&Surface<'_>>) -> Result { + pub async fn with_config( + config: RendererConfig, + instance: Option, + surface: Option<&Surface<'_>>, + ) -> Result { info!("Initializing integrated renderer system"); - + // Initialize device manager let device_manager = Arc::new(DeviceManager::new(instance, surface).await?); - + // Configure device selection based on renderer config let mut criteria = crate::device::DeviceSelectionCriteria::default(); - + // Check feature support - let has_timestamp = device_manager.adapters().iter().any(|(_, caps)| - caps.supported_features.contains(Features::TIMESTAMP_QUERY) - ); - let has_pipeline_stats = device_manager.adapters().iter().any(|(_, caps)| - caps.supported_features.contains(Features::PIPELINE_STATISTICS_QUERY) - ); + let has_timestamp = device_manager + .adapters() + .iter() + .any(|(_, caps)| caps.supported_features.contains(Features::TIMESTAMP_QUERY)); + let has_pipeline_stats = device_manager.adapters().iter().any(|(_, caps)| { + caps.supported_features + .contains(Features::PIPELINE_STATISTICS_QUERY) + }); if config.enable_profiling { if has_timestamp { @@ -152,9 +158,9 @@ impl IntegratedRenderer { warn!("Pipeline statistics not supported. Detailed profiling will be disabled."); } } - + // Use preferred adapter config if possible (DeviceSelectionCriteria doesn't directly map PowerPreference yet) - + device_manager.update_selection_criteria(criteria); // Initialize the device before getting it @@ -164,28 +170,26 @@ impl IntegratedRenderer { let device = device_manager .get_best_device() .context("Failed to get GPU device")?; - + info!("Selected GPU device: {}", device.capabilities.device_name); - - // Initialize management systems - let resource_manager = Arc::new(ResourceManager::new( - device.clone(), - )?); + + // Initialize management systems + let resource_manager = Arc::new(ResourceManager::new(device.clone())?); let memory_manager = MemoryManager::new(device.clone()); - + let shader_manager = Arc::new(ShaderManager::new(device.clone())?); - + let memory_manager_shared = Arc::new(parking_lot::Mutex::new(memory_manager)); let buffer_manager = Arc::new(BufferManager::new( device.clone(), memory_manager_shared.clone(), )); - + let pipeline_manager = Arc::new(PipelineManager::new( &device.device, wgpu::TextureFormat::Bgra8UnormSrgb, // Default surface format )); - + // Initialize profiler if enabled let profiler = if config.enable_profiling { let profiler = Arc::new(Profiler::new(device.clone())?); @@ -194,7 +198,7 @@ impl IntegratedRenderer { } else { None }; - + Ok(Self { device_manager, device, @@ -209,7 +213,7 @@ impl IntegratedRenderer { frame_count: 0, }) } - + /// Initialize the renderer (call after window creation) #[instrument(skip(self))] pub async fn initialize(&mut self) -> Result<()> { @@ -217,43 +221,46 @@ impl IntegratedRenderer { warn!("Renderer already initialized"); return Ok(()); } - + info!("Initializing renderer subsystems"); - + // Initialize shader manager (load default shaders) self.shader_manager.initialize()?; - + // Initialize pipeline manager (create default pipelines) self.pipeline_manager.initialize()?; - + // Initialize buffer manager (create default pools) self.buffer_manager.initialize()?; - + self.initialized = true; info!("Renderer initialization complete"); - + Ok(()) } - + /// Begin a new frame #[instrument(skip(self))] pub fn begin_frame(&mut self) -> Result { if !self.initialized { return Err(anyhow::anyhow!("Renderer not initialized")); } - + self.frame_count += 1; - + // Begin profiling if enabled if let Some(ref profiler) = self.profiler { profiler.begin_frame(); } - + // Create command encoder - let encoder = self.device.device.create_command_encoder(&CommandEncoderDescriptor { - label: Some(&format!("Frame {}", self.frame_count)), - }); - + let encoder = self + .device + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some(&format!("Frame {}", self.frame_count)), + }); + // Begin GPU timing let gpu_timer_id = if let Some(ref _profiler) = self.profiler { // Note: encoder is moved, so we need to handle this differently @@ -261,7 +268,7 @@ impl IntegratedRenderer { } else { None }; - + Ok(RenderContext { device: self.device.clone(), encoder, @@ -270,7 +277,7 @@ impl IntegratedRenderer { gpu_timer_id, }) } - + /// End the current frame and submit commands #[instrument(skip(self, context))] pub fn end_frame(&mut self, context: RenderContext) -> Result<()> { @@ -278,40 +285,41 @@ impl IntegratedRenderer { if let (Some(_profiler), Some(_timer_id)) = (&context.profiler, context.gpu_timer_id) { // profiler.end_gpu_timing(&mut context.encoder, timer_id); } - + // Submit command buffer let command_buffer = context.encoder.finish(); self.device.queue.submit(std::iter::once(command_buffer)); - + // End profiling if enabled if let Some(ref profiler) = self.profiler { profiler.end_frame(); } - + // Perform maintenance tasks periodically if self.frame_count % 60 == 0 { self.perform_maintenance()?; } - + Ok(()) } - + /// Get render statistics pub fn get_stats(&self) -> RenderStats { let memory_usage = self.memory_manager.lock().get_total_allocated(); let active_resources = self.resource_manager.get_active_count(); - - let (average_frame_time, shader_reloads, pipeline_switches) = if let Some(ref profiler) = self.profiler { - let report = profiler.get_performance_report(); - ( - report.frame_stats.average_frame_time, - 0, // Would need to track in shader manager - 0, // Would need to track in pipeline manager - ) - } else { - (0.0, 0, 0) - }; - + + let (average_frame_time, shader_reloads, pipeline_switches) = + if let Some(ref profiler) = self.profiler { + let report = profiler.get_performance_report(); + ( + report.frame_stats.average_frame_time, + 0, // Would need to track in shader manager + 0, // Would need to track in pipeline manager + ) + } else { + (0.0, 0, 0) + }; + RenderStats { frame_count: self.frame_count, average_frame_time, @@ -321,12 +329,12 @@ impl IntegratedRenderer { pipeline_switches, } } - + /// Get performance report (if profiling is enabled) pub fn get_performance_report(&self) -> Option { self.profiler.as_ref().map(|p| p.get_performance_report()) } - + /// Create a buffer with automatic management pub fn create_buffer(&self, size: u64, usage: BufferUsages) -> Result { let config = crate::buffer::BufferConfig { @@ -341,7 +349,7 @@ impl IntegratedRenderer { }; self.buffer_manager.create_buffer(&config) } - + /// Get a buffer by handle pub fn get_buffer(&self, handle: ResourceHandle) -> Option> { self.buffer_manager.get_buffer(handle) @@ -351,17 +359,22 @@ impl IntegratedRenderer { pub fn create_texture(&self, descriptor: &TextureDescriptor) -> DefaultKey { self.resource_manager.create_texture(descriptor) } - + /// Load and compile a shader - pub fn load_shader(&self, path: &std::path::Path, stage: crate::shader::ShaderStage, variant: crate::shader::ShaderVariant) -> Result> { + pub fn load_shader( + &self, + path: &std::path::Path, + stage: crate::shader::ShaderStage, + variant: crate::shader::ShaderVariant, + ) -> Result> { self.shader_manager.load_shader(path, stage, variant) } - + /// Create a render pipeline pub fn create_render_pipeline(&self) -> Result<()> { self.pipeline_manager.create_render_pipeline() } - + /// Get the device manager pub fn device_manager(&self) -> &Arc { &self.device_manager @@ -376,7 +389,7 @@ impl IntegratedRenderer { pub fn get_device_info(&self) -> &crate::device::GpuCapabilities { &self.device.capabilities } - + /// Get the managed device pub fn device(&self) -> &ManagedDevice { &self.device @@ -384,57 +397,60 @@ impl IntegratedRenderer { /// Check if a feature is supported pub fn supports_feature(&self, feature: Features) -> bool { - self.device.capabilities.supported_features.contains(feature) + self.device + .capabilities + .supported_features + .contains(feature) } - + /// Perform maintenance tasks #[instrument(skip(self))] fn perform_maintenance(&mut self) -> Result<()> { debug!("Performing maintenance tasks"); - + // Clean up unused resources self.resource_manager.cleanup_unused(); - + // Defragment memory pools let _ = self.memory_manager.lock().defragment(); - + // Check for shader hot-reloads if self.config.enable_shader_hot_reload { if let Err(e) = self.shader_manager.check_for_reloads() { warn!("Shader hot-reload check failed: {}", e); } } - + // Collect garbage in buffer pools self.buffer_manager.collect_garbage(); - + Ok(()) } - + /// Resize surface (call when window is resized) pub fn resize(&mut self, new_size: (u32, u32)) -> Result<()> { info!("Resizing renderer to {}x{}", new_size.0, new_size.1); - + // Update any size-dependent resources // This would typically involve recreating swap chain, depth buffers, etc. - + Ok(()) } - + /// Shutdown the renderer gracefully #[instrument(skip(self))] pub fn shutdown(&mut self) { info!("Shutting down integrated renderer"); - + if let Some(ref profiler) = self.profiler { let report = profiler.get_performance_report(); info!("Final performance report: {:#?}", report); } - + // Cleanup resources self.resource_manager.cleanup_all(); self.memory_manager.lock().cleanup(); - + self.initialized = false; info!("Renderer shutdown complete"); } @@ -450,31 +466,37 @@ impl Drop for IntegratedRenderer { impl RenderContext { /// Begin a render pass with profiling - pub fn begin_render_pass<'a>(&'a mut self, descriptor: &RenderPassDescriptor<'a, '_>) -> RenderPass<'a> { + pub fn begin_render_pass<'a>( + &'a mut self, + descriptor: &RenderPassDescriptor<'a, '_>, + ) -> RenderPass<'a> { // Begin CPU timing if profiler is available if let Some(ref profiler) = self.profiler { profiler.cpu_profiler.begin_section("render_pass"); } - + self.encoder.begin_render_pass(descriptor) } - + /// End render pass timing pub fn end_render_pass(&self) { if let Some(ref profiler) = self.profiler { profiler.cpu_profiler.end_section("render_pass"); } } - + /// Begin compute pass with profiling - pub fn begin_compute_pass<'a>(&'a mut self, descriptor: &ComputePassDescriptor<'a>) -> ComputePass<'a> { + pub fn begin_compute_pass<'a>( + &'a mut self, + descriptor: &ComputePassDescriptor<'a>, + ) -> ComputePass<'a> { if let Some(ref profiler) = self.profiler { profiler.cpu_profiler.begin_section("compute_pass"); } - + self.encoder.begin_compute_pass(descriptor) } - + /// End compute pass timing pub fn end_compute_pass(&self) { if let Some(ref profiler) = self.profiler { @@ -499,55 +521,55 @@ impl<'a> RendererBuilder<'a> { surface: None, } } - + /// Set the surface for compatibility checking pub fn with_surface(mut self, surface: &'a Surface<'a>) -> Self { self.surface = Some(surface); self } - + /// Set the wgpu Instance to use pub fn with_instance(mut self, instance: Instance) -> Self { self.instance = Some(instance); self } - + /// Enable or disable profiling pub fn with_profiling(mut self, enabled: bool) -> Self { self.config.enable_profiling = enabled; self } - + /// Enable or disable detailed profiling pub fn with_detailed_profiling(mut self, enabled: bool) -> Self { self.config.detailed_profiling = enabled; self } - + /// Set memory allocation strategy pub fn with_memory_strategy(mut self, strategy: AllocationStrategy) -> Self { self.config.memory_strategy = strategy; self } - + /// Set maximum memory pool size pub fn with_max_memory_pool_size(mut self, size: u64) -> Self { self.config.max_memory_pool_size = size; self } - + /// Set preferred GPU adapter pub fn with_preferred_adapter(mut self, preference: PowerPreference) -> Self { self.config.preferred_adapter = Some(preference); self } - + /// Enable or disable validation layers pub fn with_validation(mut self, enabled: bool) -> Self { self.config.enable_validation = enabled; self } - + /// Build the integrated renderer pub async fn build(self) -> Result { IntegratedRenderer::with_config(self.config, self.instance, self.surface).await @@ -571,7 +593,7 @@ macro_rules! create_renderer { .build() .await }; - + (release) => { RendererBuilder::new() .with_profiling(false) @@ -579,7 +601,7 @@ macro_rules! create_renderer { .build() .await }; - + (performance) => { RendererBuilder::new() .with_profiling(true) @@ -589,4 +611,4 @@ macro_rules! create_renderer { .build() .await }; -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/lib.rs b/crates/strato-renderer/src/lib.rs index b865c09..0213cc7 100644 --- a/crates/strato-renderer/src/lib.rs +++ b/crates/strato-renderer/src/lib.rs @@ -19,7 +19,7 @@ pub mod device; pub mod font_config; pub mod font_system; pub mod glyph_atlas; -pub mod gpu; // Modular GPU pipeline +pub mod gpu; // Modular GPU pipeline pub mod memory; pub mod pipeline; pub mod profiler; @@ -34,20 +34,17 @@ pub mod backend; pub mod integration; // Re-export commonly used types -pub use backend::Backend; pub use backend::commands::RenderCommand; +pub use backend::Backend; pub use batch::RenderBatch; -pub use buffer::{BufferManager, DynamicBuffer, BufferPool}; -pub use device::{ManagedDevice, DeviceManager, AdapterInfo}; -pub use integration::{IntegratedRenderer, RendererBuilder, RenderContext, RenderStats}; -pub use memory::{MemoryManager, MemoryPool, AllocationStrategy}; +pub use buffer::{BufferManager, BufferPool, DynamicBuffer}; +pub use device::{AdapterInfo, DeviceManager, ManagedDevice}; +pub use integration::{IntegratedRenderer, RenderContext, RenderStats, RendererBuilder}; +pub use memory::{AllocationStrategy, MemoryManager, MemoryPool}; pub use pipeline::{PipelineManager, RenderGraph, RenderNode}; -pub use profiler::{Profiler, PerformanceReport, FrameStats}; -pub use resources::{ResourceManager, ResourceHandle, ResourceType}; -pub use shader::{ShaderManager, ShaderSource, CompiledShader}; - - - +pub use profiler::{FrameStats, PerformanceReport, Profiler}; +pub use resources::{ResourceHandle, ResourceManager, ResourceType}; +pub use shader::{CompiledShader, ShaderManager, ShaderSource}; /// Renderer configuration #[derive(Debug, Clone)] diff --git a/crates/strato-renderer/src/memory.rs b/crates/strato-renderer/src/memory.rs index d430bb5..0b7d2ce 100644 --- a/crates/strato-renderer/src/memory.rs +++ b/crates/strato-renderer/src/memory.rs @@ -9,17 +9,20 @@ //! - Resource streaming and prefetching //! - Memory usage profiling and analytics -use std::collections::{HashMap, BTreeMap, BinaryHeap}; -use std::sync::{Arc, atomic::{AtomicU64, AtomicBool, Ordering}}; -use std::time::{Duration, Instant}; -use std::cmp::Ordering as CmpOrdering; -use parking_lot::RwLock; -use wgpu::{Device, Buffer, BufferUsages, BufferDescriptor}; -use anyhow::{Result, Context}; -use tracing::{info, warn, debug, instrument}; -use serde::{Serialize, Deserialize}; -use strato_core::{strato_debug, strato_warn, strato_error_rate_limited, logging::LogCategory}; use crate::device::{ManagedDevice, OptimizationHints}; +use anyhow::{Context, Result}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering as CmpOrdering; +use std::collections::{BTreeMap, BinaryHeap, HashMap}; +use std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, +}; +use std::time::{Duration, Instant}; +use strato_core::{logging::LogCategory, strato_debug, strato_error_rate_limited, strato_warn}; +use tracing::{debug, info, instrument, warn}; +use wgpu::{Buffer, BufferDescriptor, BufferUsages, Device}; /// Memory allocation strategy #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -97,7 +100,10 @@ impl PartialOrd for FreeRegion { impl Ord for FreeRegion { fn cmp(&self, other: &Self) -> CmpOrdering { // For best-fit allocation with BinaryHeap (max-heap), invert size comparison - other.size.cmp(&self.size).then(self.offset.cmp(&other.offset)) + other + .size + .cmp(&self.size) + .then(self.offset.cmp(&other.offset)) } } @@ -145,30 +151,35 @@ impl MemoryPool { last_defrag: RwLock::new(Instant::now()), } } - + /// Allocate memory from this pool - pub fn allocate(&mut self, size: u64, alignment: u64, device: &Device) -> Result> { + pub fn allocate( + &mut self, + size: u64, + alignment: u64, + device: &Device, + ) -> Result> { let aligned_size = Self::align_size(size, alignment); - + // Try to find a suitable free region if let Some(region) = self.find_free_region(aligned_size, alignment) { return self.allocate_from_region(region, aligned_size, alignment, device); } - + // Need to create a new block self.create_new_block(aligned_size, alignment, device) } - + /// Find a suitable free region fn find_free_region(&mut self, size: u64, alignment: u64) -> Option { let mut best_region: Option = None; let mut temp_regions = Vec::new(); - + // Extract regions and find the best fit while let Some(region) = self.free_regions.pop() { let aligned_offset = Self::align_offset(region.offset, alignment); let required_size = aligned_offset - region.offset + size; - + if region.size >= required_size { if best_region.is_none() || region.size < best_region.as_ref().unwrap().size { if let Some(prev_best) = best_region.take() { @@ -182,15 +193,15 @@ impl MemoryPool { temp_regions.push(region); } } - + // Put back the regions we didn't use for region in temp_regions { self.free_regions.push(region); } - + best_region } - + /// Allocate from an existing free region fn allocate_from_region( &mut self, @@ -201,7 +212,7 @@ impl MemoryPool { ) -> Result> { let aligned_offset = Self::align_offset(region.offset, alignment); let padding = aligned_offset - region.offset; - + // Create padding region if needed if padding > 0 { self.free_regions.push(FreeRegion { @@ -209,7 +220,7 @@ impl MemoryPool { size: padding, }); } - + // Create remaining region if any let remaining_size = region.size - padding - size; if remaining_size > 0 { @@ -218,10 +229,10 @@ impl MemoryPool { size: remaining_size, }); } - + // Find the buffer that contains this region let buffer = self.find_buffer_for_offset(aligned_offset)?; - + let block = Arc::new(MemoryBlock { buffer, size, @@ -234,26 +245,34 @@ impl MemoryPool { access_count: AtomicU64::new(0), is_mapped: AtomicBool::new(false), }); - + self.allocated_regions.insert(aligned_offset, size); self.used_size.fetch_add(size, Ordering::Relaxed); self.allocation_count.fetch_add(1, Ordering::Relaxed); - + Ok(block) } - + /// Create a new buffer block - fn create_new_block(&mut self, size: u64, alignment: u64, device: &Device) -> Result> { + fn create_new_block( + &mut self, + size: u64, + alignment: u64, + device: &Device, + ) -> Result> { let block_size = std::cmp::max(size, self.min_block_size); let block_size = std::cmp::min(block_size, self.max_block_size); - + let buffer = Arc::new(device.create_buffer(&BufferDescriptor { - label: Some(&format!("MemoryPool-{:?}-{:?}", self.usage_pattern, self.tier)), + label: Some(&format!( + "MemoryPool-{:?}-{:?}", + self.usage_pattern, self.tier + )), size: block_size, usage: self.get_buffer_usage(), mapped_at_creation: false, })); - + let block = Arc::new(MemoryBlock { buffer, size, @@ -266,7 +285,7 @@ impl MemoryPool { access_count: AtomicU64::new(0), is_mapped: AtomicBool::new(false), }); - + // Add remaining space to free regions if block_size > size { self.free_regions.push(FreeRegion { @@ -274,16 +293,16 @@ impl MemoryPool { size: block_size - size, }); } - + self.blocks.push(block.clone()); self.allocated_regions.insert(0, size); self.total_size.fetch_add(block_size, Ordering::Relaxed); self.used_size.fetch_add(size, Ordering::Relaxed); self.allocation_count.fetch_add(1, Ordering::Relaxed); - + Ok(block) } - + /// Deallocate a memory block pub fn deallocate(&mut self, block: &MemoryBlock) { if let Some(size) = self.allocated_regions.remove(&block.offset) { @@ -291,23 +310,23 @@ impl MemoryPool { offset: block.offset, size, }); - + self.used_size.fetch_sub(size, Ordering::Relaxed); self.deallocation_count.fetch_add(1, Ordering::Relaxed); - + // Coalesce adjacent free regions self.coalesce_free_regions(); } } - + /// Coalesce adjacent free regions to reduce fragmentation fn coalesce_free_regions(&mut self) { let mut regions: Vec<_> = self.free_regions.drain().collect(); regions.sort_by_key(|r| r.offset); - + let mut coalesced = Vec::new(); let mut current: Option = None; - + for region in regions { match current.take() { None => current = Some(region), @@ -324,54 +343,61 @@ impl MemoryPool { } } } - + if let Some(last) = current { coalesced.push(last); } - + self.free_regions = coalesced.into_iter().collect(); } - + /// Calculate fragmentation ratio pub fn calculate_fragmentation(&self) -> f32 { let total = self.total_size.load(Ordering::Relaxed); if total == 0 { return 0.0; } - + let _used = self.used_size.load(Ordering::Relaxed); let free_regions = self.free_regions.len(); - + // Fragmentation increases with number of free regions and decreases with usage let fragmentation = (free_regions as f32 * 100.0) / (total as f32 / 1024.0); fragmentation.min(100.0) } - + /// Get buffer usage flags for this pool fn get_buffer_usage(&self) -> BufferUsages { match self.usage_pattern { - UsagePattern::Transient => BufferUsages::VERTEX | BufferUsages::INDEX | BufferUsages::COPY_DST, - UsagePattern::FramePersistent => BufferUsages::UNIFORM | BufferUsages::STORAGE | BufferUsages::COPY_DST, - UsagePattern::Persistent => BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + UsagePattern::Transient => { + BufferUsages::VERTEX | BufferUsages::INDEX | BufferUsages::COPY_DST + } + UsagePattern::FramePersistent => { + BufferUsages::UNIFORM | BufferUsages::STORAGE | BufferUsages::COPY_DST + } + UsagePattern::Persistent => { + BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC + } UsagePattern::Static => BufferUsages::VERTEX | BufferUsages::INDEX, UsagePattern::Streaming => BufferUsages::COPY_DST | BufferUsages::COPY_SRC, } } - + /// Find buffer that contains the given offset fn find_buffer_for_offset(&self, _offset: u64) -> Result> { // This is a simplified implementation // In practice, you'd need to track which buffer contains which offset range - self.blocks.first() + self.blocks + .first() .map(|block| block.buffer.clone()) .context("No buffer available for offset") } - + /// Align size to the given alignment fn align_size(size: u64, alignment: u64) -> u64 { (size + alignment - 1) & !(alignment - 1) } - + /// Align offset to the given alignment fn align_offset(offset: u64, alignment: u64) -> u64 { (offset + alignment - 1) & !(alignment - 1) @@ -407,9 +433,9 @@ impl MemoryManager { /// Create a new memory manager pub fn new(device: Arc) -> Self { let optimization_hints = device.optimization_hints.clone(); - + let mut pools = HashMap::new(); - + // Create pools for different usage patterns and tiers for &pattern in &[ UsagePattern::Transient, @@ -418,16 +444,21 @@ impl MemoryManager { UsagePattern::Static, UsagePattern::Streaming, ] { - for &tier in &[MemoryTier::HighSpeed, MemoryTier::Standard, MemoryTier::Shared] { - let (min_size, max_size, strategy) = Self::get_pool_config(pattern, tier, &optimization_hints); - + for &tier in &[ + MemoryTier::HighSpeed, + MemoryTier::Standard, + MemoryTier::Shared, + ] { + let (min_size, max_size, strategy) = + Self::get_pool_config(pattern, tier, &optimization_hints); + pools.insert( (pattern, tier), MemoryPool::new(pattern, tier, min_size, max_size, strategy), ); } } - + Self { device, pools, @@ -438,7 +469,7 @@ impl MemoryManager { optimization_hints, } } - + /// Get pool configuration for usage pattern and tier fn get_pool_config( pattern: UsagePattern, @@ -446,21 +477,21 @@ impl MemoryManager { hints: &OptimizationHints, ) -> (u64, u64, AllocationStrategy) { let base_alignment = hints.preferred_buffer_alignment as u64; - + match (pattern, tier) { (UsagePattern::Transient, _) => ( - 4 * 1024, // 4KB min - 16 * 1024 * 1024, // 16MB max + 4 * 1024, // 4KB min + 16 * 1024 * 1024, // 16MB max AllocationStrategy::Linear, ), (UsagePattern::FramePersistent, MemoryTier::HighSpeed) => ( - 64 * 1024, // 64KB min - 64 * 1024 * 1024, // 64MB max + 64 * 1024, // 64KB min + 64 * 1024 * 1024, // 64MB max AllocationStrategy::BestFit, ), (UsagePattern::Persistent, _) => ( - 1024 * 1024, // 1MB min - 256 * 1024 * 1024, // 256MB max + 1024 * 1024, // 1MB min + 256 * 1024 * 1024, // 256MB max AllocationStrategy::Buddy, ), (UsagePattern::Static, _) => ( @@ -469,8 +500,8 @@ impl MemoryManager { AllocationStrategy::FirstFit, ), (UsagePattern::Streaming, _) => ( - 1024 * 1024, // 1MB min - 128 * 1024 * 1024, // 128MB max + 1024 * 1024, // 1MB min + 128 * 1024 * 1024, // 128MB max AllocationStrategy::Slab, ), _ => ( @@ -480,7 +511,7 @@ impl MemoryManager { ), } } - + /// Allocate memory with specific usage pattern #[instrument(skip(self))] pub fn allocate( @@ -491,14 +522,16 @@ impl MemoryManager { tier: MemoryTier, ) -> Result> { let pool_key = (usage_pattern, tier); - + // Get pool reference and try allocation let allocation_result = { - let pool = self.pools.get_mut(&pool_key) + let pool = self + .pools + .get_mut(&pool_key) .context("Pool not found for usage pattern and tier")?; pool.allocate(size, alignment, &self.device.device) }; - + match allocation_result { Ok(block) => { let mut stats = self.allocation_stats.write(); @@ -506,20 +539,22 @@ impl MemoryManager { stats.current_usage += size; stats.allocation_count += 1; stats.peak_usage = stats.peak_usage.max(stats.current_usage); - + Ok(block) } Err(e) => { let mut stats = self.allocation_stats.write(); stats.failed_allocations += 1; - + // Try memory pressure relief if self.auto_defrag_enabled.load(Ordering::Relaxed) { drop(stats); self.relieve_memory_pressure()?; - + // Retry allocation - let pool = self.pools.get_mut(&pool_key) + let pool = self + .pools + .get_mut(&pool_key) .context("Pool not found for usage pattern and tier")?; pool.allocate(size, alignment, &self.device.device) } else { @@ -528,30 +563,27 @@ impl MemoryManager { } } } - + /// Deallocate memory block pub fn deallocate(&mut self, block: Arc) { - let key = ( - self.classify_usage_pattern(&block), - block.tier, - ); - + let key = (self.classify_usage_pattern(&block), block.tier); + if let Some(pool) = self.pools.get_mut(&key) { let size = block.size; pool.deallocate(&block); - + let mut stats = self.allocation_stats.write(); stats.total_freed += size; stats.current_usage = stats.current_usage.saturating_sub(size); stats.deallocation_count += 1; } } - + /// Classify usage pattern from block characteristics fn classify_usage_pattern(&self, block: &MemoryBlock) -> UsagePattern { let age = block.allocation_time.elapsed(); let access_count = block.access_count.load(Ordering::Relaxed); - + if age < Duration::from_millis(16) { UsagePattern::Transient } else if age < Duration::from_millis(160) && access_count > 10 { @@ -562,17 +594,19 @@ impl MemoryManager { UsagePattern::Persistent } } - + /// Relieve memory pressure through cleanup and defragmentation #[instrument(skip(self))] pub fn relieve_memory_pressure(&mut self) -> Result<()> { info!("Relieving memory pressure..."); - + // Cleanup unused resources self.cleanup_unused_resources(); - + // Defragment pools with high fragmentation - let pools_to_defrag: Vec<_> = self.pools.values_mut() + let pools_to_defrag: Vec<_> = self + .pools + .values_mut() .filter_map(|pool| { let fragmentation = pool.calculate_fragmentation(); if fragmentation > 50.0 { @@ -582,100 +616,107 @@ impl MemoryManager { } }) .collect(); - + for pool_ptr in pools_to_defrag { unsafe { self.defragment_pool(&mut *pool_ptr)?; } } - + let mut stats = self.allocation_stats.write(); stats.defragmentation_count += 1; - + Ok(()) } - + /// Cleanup unused resources fn cleanup_unused_resources(&mut self) { let now = Instant::now(); let cleanup_threshold = Duration::from_secs(30); - + for pool in self.pools.values_mut() { pool.blocks.retain(|block| { let age = now.duration_since(block.last_access); let access_count = block.access_count.load(Ordering::Relaxed); - + // Keep blocks that are recently accessed or frequently used age < cleanup_threshold || access_count > 100 }); } - + *self.last_cleanup.write() = now; } - + /// Defragment a memory pool fn defragment_pool(&mut self, pool: &mut MemoryPool) -> Result<()> { - debug!("Defragmenting pool: {:?}-{:?}", pool.usage_pattern, pool.tier); - + debug!( + "Defragmenting pool: {:?}-{:?}", + pool.usage_pattern, pool.tier + ); + // Coalesce free regions pool.coalesce_free_regions(); - + // Update fragmentation ratio let fragmentation = pool.calculate_fragmentation(); - pool.fragmentation_ratio.store((fragmentation * 1000.0) as u64, Ordering::Relaxed); - + pool.fragmentation_ratio + .store((fragmentation * 1000.0) as u64, Ordering::Relaxed); + *pool.last_defrag.write() = Instant::now(); - + Ok(()) } - + /// Get memory statistics pub fn get_stats(&self) -> AllocationStats { let mut stats = self.allocation_stats.read().clone(); - + // Calculate average fragmentation across all pools - let total_fragmentation: f32 = self.pools.values() + let total_fragmentation: f32 = self + .pools + .values() .map(|pool| pool.calculate_fragmentation()) .sum(); - + stats.average_fragmentation = if self.pools.is_empty() { 0.0 } else { total_fragmentation / self.pools.len() as f32 }; - + stats } - + /// Check if memory pressure relief is needed pub fn needs_pressure_relief(&self) -> bool { let stats = self.allocation_stats.read(); let threshold = self.memory_pressure_threshold.load(Ordering::Relaxed); - + stats.current_usage > threshold || stats.average_fragmentation > 75.0 } - + /// Set memory pressure threshold pub fn set_pressure_threshold(&self, threshold: u64) { - self.memory_pressure_threshold.store(threshold, Ordering::Relaxed); + self.memory_pressure_threshold + .store(threshold, Ordering::Relaxed); } - + /// Enable or disable automatic defragmentation pub fn set_auto_defrag(&self, enabled: bool) { self.auto_defrag_enabled.store(enabled, Ordering::Relaxed); } - + /// Get total allocated memory (integration method) pub fn get_total_allocated(&self) -> u64 { let stats = self.get_stats(); stats.total_allocated } - + /// Defragment memory (integration method) pub fn defragment(&mut self) -> Result<()> { self.relieve_memory_pressure() } - + /// Cleanup memory (integration method) pub fn cleanup(&mut self) -> Result<()> { self.relieve_memory_pressure() @@ -685,27 +726,36 @@ impl MemoryManager { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_free_region_ordering() { let mut regions = BinaryHeap::new(); - - regions.push(FreeRegion { offset: 100, size: 50 }); - regions.push(FreeRegion { offset: 0, size: 100 }); - regions.push(FreeRegion { offset: 200, size: 25 }); - + + regions.push(FreeRegion { + offset: 100, + size: 50, + }); + regions.push(FreeRegion { + offset: 0, + size: 100, + }); + regions.push(FreeRegion { + offset: 200, + size: 25, + }); + // Should pop smallest size first (best fit) assert_eq!(regions.pop().unwrap().size, 25); assert_eq!(regions.pop().unwrap().size, 50); assert_eq!(regions.pop().unwrap().size, 100); } - + #[test] fn test_alignment() { assert_eq!(MemoryPool::align_size(100, 256), 256); assert_eq!(MemoryPool::align_size(256, 256), 256); assert_eq!(MemoryPool::align_size(257, 256), 512); - + assert_eq!(MemoryPool::align_offset(100, 256), 256); assert_eq!(MemoryPool::align_offset(256, 256), 256); assert_eq!(MemoryPool::align_offset(257, 256), 512); diff --git a/crates/strato-renderer/src/pipeline.rs b/crates/strato-renderer/src/pipeline.rs index 35919a3..44a89b0 100644 --- a/crates/strato-renderer/src/pipeline.rs +++ b/crates/strato-renderer/src/pipeline.rs @@ -10,20 +10,20 @@ //! - Hot-swappable pipeline configurations //! - Cross-platform pipeline optimization +use crate::vertex::Vertex; +use anyhow::{Context, Result}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use parking_lot::RwLock; use wgpu::{ - Device, RenderPipeline, ComputePipeline, PipelineLayout, BindGroupLayout, ShaderModule, - BindGroupLayoutDescriptor, BindGroupLayoutEntry, ShaderStages, BindingType, BufferBindingType, - TextureSampleType, TextureViewDimension, PipelineLayoutDescriptor, RenderPipelineDescriptor, - VertexState, FragmentState, ColorTargetState, BlendState, ColorWrites, PrimitiveState, - MultisampleState, Buffer, BindGroup, BindGroupDescriptor, BindGroupEntry + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingType, BlendState, Buffer, BufferBindingType, ColorTargetState, + ColorWrites, ComputePipeline, Device, FragmentState, MultisampleState, PipelineLayout, + PipelineLayoutDescriptor, PrimitiveState, RenderPipeline, RenderPipelineDescriptor, + ShaderModule, ShaderStages, TextureSampleType, TextureViewDimension, VertexState, }; -use anyhow::{Result, Context}; -use serde::{Serialize, Deserialize}; -use crate::vertex::Vertex; /// Uniform data for the UI shader #[repr(C)] @@ -39,7 +39,7 @@ impl UIUniforms { pub fn new(width: f32, height: f32, time: f32) -> Self { // Create orthographic projection matrix let view_proj = Self::orthographic_projection(0.0, width, height, 0.0, -1.0, 1.0); - + Self { view_proj, screen_size: [width, height], @@ -64,7 +64,12 @@ impl UIUniforms { [2.0 / width, 0.0, 0.0, 0.0], [0.0, -2.0 / height, 0.0, 0.0], [0.0, 0.0, -1.0 / depth, 0.0], - [-(right + left) / width, -(top + bottom) / height, -near / depth, 1.0], + [ + -(right + left) / width, + -(top + bottom) / height, + -near / depth, + 1.0, + ], ] } } @@ -191,7 +196,8 @@ impl UIPipeline { view_formats: &[], }); - let default_texture_view = default_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let default_texture_view = + default_texture.create_view(&wgpu::TextureViewDescriptor::default()); // Create sampler let sampler = device.create_sampler(&wgpu::SamplerDescriptor { @@ -275,7 +281,6 @@ pub struct TextPipeline { impl TextPipeline { /// Create a new text render pipeline pub fn new(device: &Device, surface_format: wgpu::TextureFormat) -> Self { - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Text Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("shaders/ui.wgsl").into()), @@ -394,17 +399,17 @@ impl RenderGraph { execution_order: Vec::new(), } } - + pub fn add_node(&mut self, node: RenderNode) { self.nodes.push(node); self.update_execution_order(); } - + fn update_execution_order(&mut self) { // Simple topological sort for now self.execution_order = (0..self.nodes.len()).collect(); } - + pub fn get_execution_order(&self) -> &[usize] { &self.execution_order } @@ -414,7 +419,7 @@ impl RenderGraph { pub struct PipelineManager { pub ui_pipeline: UIPipeline, // Text rendering is now handled via UI pipeline or generic sprite batching - // pub text_pipeline: TextPipeline, + // pub text_pipeline: TextPipeline, render_graph: RenderGraph, } @@ -435,13 +440,13 @@ impl PipelineManager { pub fn update_uniforms(&self, queue: &wgpu::Queue, uniforms: &UIUniforms) { self.ui_pipeline.update_uniforms(queue, uniforms); } - + /// Initialize the pipeline manager (integration method) pub fn initialize(&self) -> anyhow::Result<()> { tracing::info!("Pipeline manager initialized"); Ok(()) } - + /// Create render pipeline (placeholder integration method) pub fn create_render_pipeline(&self) -> anyhow::Result<()> { // Placeholder - would create additional pipelines as needed @@ -463,7 +468,7 @@ mod tests { #[test] fn test_orthographic_projection() { let proj = UIUniforms::orthographic_projection(0.0, 800.0, 600.0, 0.0, -1.0, 1.0); - + // Check that it's a valid orthographic projection matrix assert_eq!(proj[0][0], 2.0 / 800.0); // X scale assert_eq!(proj[1][1], -2.0 / 600.0); // Y scale (flipped for screen coordinates) diff --git a/crates/strato-renderer/src/resources.rs b/crates/strato-renderer/src/resources.rs index d7b4e20..20e9d46 100644 --- a/crates/strato-renderer/src/resources.rs +++ b/crates/strato-renderer/src/resources.rs @@ -10,14 +10,14 @@ //! - Memory pressure detection and adaptive strategies //! - Resource dependency tracking and batch operations -use std::collections::{HashMap, BTreeSet, VecDeque}; -use slotmap::{SlotMap, DefaultKey}; -use std::sync::{Arc, Weak, Mutex}; -use std::time::{Duration, Instant}; -use parking_lot::RwLock; -use wgpu::{Device, Buffer, BufferUsages, BufferDescriptor, MapMode, Maintain, *}; use anyhow::Result; -use serde::{Serialize, Deserialize}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use slotmap::{DefaultKey, SlotMap}; +use std::collections::{BTreeSet, HashMap, VecDeque}; +use std::sync::{Arc, Mutex, Weak}; +use std::time::{Duration, Instant}; +use wgpu::{Buffer, BufferDescriptor, BufferUsages, Device, Maintain, MapMode, *}; use crate::device::ManagedDevice; @@ -64,7 +64,7 @@ pub enum ResourceType { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MemoryCategory { Vertex, - Index, + Index, Uniform, Storage, Texture2D, @@ -78,10 +78,10 @@ pub enum MemoryCategory { /// Resource priority for memory management decisions #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ResourcePriority { - Critical = 4, // Never evict - High = 3, // Evict only under extreme pressure - Medium = 2, // Default priority - Low = 1, // First to be evicted + Critical = 4, // Never evict + High = 3, // Evict only under extreme pressure + Medium = 2, // Default priority + Low = 1, // First to be evicted Disposable = 0, // Can be recreated easily } @@ -233,11 +233,7 @@ impl BufferPool { // Pre-allocate initial buffers for i in 0..config.initial_size { - let buffer = PooledBuffer::new( - device, - &config, - Some(&format!("PooledBuffer_{}", i)), - ); + let buffer = PooledBuffer::new(device, &config, Some(&format!("PooledBuffer_{}", i))); pool.total_allocated += buffer.size; pool.available.push_back(buffer); } @@ -277,7 +273,7 @@ impl BufferPool { i += 1; } } - + // Update peak usage let current_usage = self.in_use.len() as u64 * self.config.buffer_size; self.peak_usage = self.peak_usage.max(current_usage); @@ -286,12 +282,12 @@ impl BufferPool { /// Clean up old unused buffers pub fn cleanup(&mut self, max_age: std::time::Duration) { let now = std::time::Instant::now(); - self.available.retain(|buffer| { - now.duration_since(buffer.last_used) < max_age - }); - + self.available + .retain(|buffer| now.duration_since(buffer.last_used) < max_age); + // Recalculate total allocation - self.total_allocated = (self.available.len() + self.in_use.len()) as u64 * self.config.buffer_size; + self.total_allocated = + (self.available.len() + self.in_use.len()) as u64 * self.config.buffer_size; } /// Get pool statistics @@ -358,7 +354,7 @@ impl TextureAtlas { }); let view = texture.create_view(&TextureViewDescriptor::default()); - + let sampler = device.create_sampler(&SamplerDescriptor { label: Some("AtlasSampler"), address_mode_u: AddressMode::ClampToEdge, @@ -441,7 +437,7 @@ impl TextureAtlas { let id = self.next_id; self.next_id += 1; - + self.allocations.insert(id, allocation.clone()); Some(AtlasHandle { @@ -491,7 +487,7 @@ impl TextureAtlas { /// Clean up unused allocations pub fn cleanup(&mut self) { let mut to_remove = Vec::new(); - + for (&id, allocation) in &self.allocations { if Arc::strong_count(&allocation.ref_count) == 1 { to_remove.push(id); @@ -532,10 +528,12 @@ impl TextureAtlas { /// Get atlas statistics pub fn stats(&self) -> AtlasStats { let total_area = self.size * self.size; - let used_area: u32 = self.allocations.values() + let used_area: u32 = self + .allocations + .values() .map(|alloc| alloc.region.width * alloc.region.height) .sum(); - + AtlasStats { size: self.size, allocations: self.allocations.len(), @@ -590,16 +588,16 @@ pub struct AtlasStats { /// Comprehensive resource manager pub struct ResourceManager { managed_device: Arc, - + // Buffer pools vertex_pool: Mutex, index_pool: Mutex, uniform_pool: Mutex, - + // Texture management texture_atlas: RwLock, textures: RwLock>>, - + // Resource tracking memory_usage: Mutex, cleanup_interval: std::time::Duration, @@ -636,7 +634,7 @@ impl ResourceManager { let device_ref = &managed_device.device; let _queue_ref = &managed_device.queue; - + Ok(Self { vertex_pool: Mutex::new(BufferPool::new(device_ref, vertex_config)?), index_pool: Mutex::new(BufferPool::new(device_ref, index_config)?), @@ -683,7 +681,7 @@ impl ResourceManager { let mut textures = self.textures.write(); textures.insert(Arc::new(texture)) } - + /// Get a texture by handle pub fn get_texture(&self, handle: DefaultKey) -> Option> { let textures = self.textures.read(); @@ -694,21 +692,30 @@ impl ResourceManager { pub fn cleanup(&self) { let mut last_cleanup = self.last_cleanup.lock().unwrap(); let now = std::time::Instant::now(); - + if now.duration_since(*last_cleanup) > self.cleanup_interval { // Clean up buffer pools - self.vertex_pool.lock().unwrap().cleanup(self.cleanup_interval); - self.index_pool.lock().unwrap().cleanup(self.cleanup_interval); - self.uniform_pool.lock().unwrap().cleanup(self.cleanup_interval); - + self.vertex_pool + .lock() + .unwrap() + .cleanup(self.cleanup_interval); + self.index_pool + .lock() + .unwrap() + .cleanup(self.cleanup_interval); + self.uniform_pool + .lock() + .unwrap() + .cleanup(self.cleanup_interval); + // Clean up atlas self.texture_atlas.write().cleanup(); - + // Release unused buffers self.vertex_pool.lock().unwrap().release_unused(); self.index_pool.lock().unwrap().release_unused(); self.uniform_pool.lock().unwrap().release_unused(); - + *last_cleanup = now; } } @@ -729,28 +736,28 @@ impl ResourceManager { pub fn force_gc(&self) { // More aggressive cleanup let long_duration = std::time::Duration::from_secs(0); - + self.vertex_pool.lock().unwrap().cleanup(long_duration); self.index_pool.lock().unwrap().cleanup(long_duration); self.uniform_pool.lock().unwrap().cleanup(long_duration); - + self.texture_atlas.write().cleanup(); - + self.vertex_pool.lock().unwrap().release_unused(); self.index_pool.lock().unwrap().release_unused(); self.uniform_pool.lock().unwrap().release_unused(); } - + /// Get active resource count for integration pub fn get_active_count(&self) -> usize { self.textures.read().len() } - + /// Cleanup unused resources (integration method) pub fn cleanup_unused(&self) { self.cleanup(); } - + /// Cleanup all resources (integration method) pub fn cleanup_all(&self) { self.force_gc(); @@ -771,21 +778,21 @@ pub struct ResourceStats { impl ResourceStats { /// Get total memory usage in bytes pub fn total_memory(&self) -> u64 { - self.vertex_pool.total_allocated + - self.index_pool.total_allocated + - self.uniform_pool.total_allocated + - self.memory_usage.texture_memory + self.vertex_pool.total_allocated + + self.index_pool.total_allocated + + self.uniform_pool.total_allocated + + self.memory_usage.texture_memory } /// Get total resource count pub fn total_resources(&self) -> usize { - self.vertex_pool.available_count + - self.vertex_pool.in_use_count + - self.index_pool.available_count + - self.index_pool.in_use_count + - self.uniform_pool.available_count + - self.uniform_pool.in_use_count + - self.texture_count + self.vertex_pool.available_count + + self.vertex_pool.in_use_count + + self.index_pool.available_count + + self.index_pool.in_use_count + + self.uniform_pool.available_count + + self.uniform_pool.in_use_count + + self.texture_count } } @@ -795,10 +802,25 @@ mod tests { #[test] fn test_region_ordering() { - let region1 = Region { x: 0, y: 0, width: 10, height: 10 }; - let region2 = Region { x: 0, y: 0, width: 20, height: 10 }; - let region3 = Region { x: 10, y: 0, width: 10, height: 10 }; - + let region1 = Region { + x: 0, + y: 0, + width: 10, + height: 10, + }; + let region2 = Region { + x: 0, + y: 0, + width: 20, + height: 10, + }; + let region3 = Region { + x: 10, + y: 0, + width: 10, + height: 10, + }; + assert!(region1 < region2); assert!(region1 < region3); } @@ -807,15 +829,20 @@ mod tests { fn test_atlas_handle_uv() { let handle = AtlasHandle { id: 1, - region: Region { x: 100, y: 200, width: 50, height: 75 }, + region: Region { + x: 100, + y: 200, + width: 50, + height: 75, + }, atlas_size: 1000, _ref: Weak::new(), }; - + let (u, v, w, h) = handle.uv_coords(); assert_eq!(u, 0.1); assert_eq!(v, 0.2); assert_eq!(w, 0.05); assert_eq!(h, 0.075); } -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/shader.rs b/crates/strato-renderer/src/shader.rs index 265507f..af737fb 100644 --- a/crates/strato-renderer/src/shader.rs +++ b/crates/strato-renderer/src/shader.rs @@ -10,20 +10,23 @@ //! - Shader debugging and validation tools //! - Modular shader composition system +use anyhow::{bail, Context, Result}; +use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; +use parking_lot::{Mutex, RwLock}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; +use std::fs; use std::path::{Path, PathBuf}; -use std::sync::{Arc, atomic::{AtomicU64, AtomicBool, Ordering}}; +use std::sync::mpsc; +use std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, +}; use std::time::{Duration, Instant, SystemTime}; -use std::fs; -use parking_lot::{RwLock, Mutex}; -use wgpu::*; -use anyhow::{Result, Context, bail}; use tracing::{info, instrument}; -use serde::{Serialize, Deserialize}; -use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event}; -use std::sync::mpsc; -use regex::Regex; -use sha2::{Sha256, Digest}; +use wgpu::*; use crate::device::ManagedDevice; use crate::resources::ResourceHandle; @@ -135,22 +138,22 @@ pub struct ShaderManager { source_cache: RwLock>>, dependency_graph: RwLock>, compilation_stats: RwLock, - + // Hot-reload system file_watcher: Arc>>, hot_reload_enabled: AtomicBool, hot_reload_receiver: Arc>>>, watched_directories: RwLock>, - + // Shader preprocessing include_directories: RwLock>, global_macros: RwLock>, preprocessor_cache: RwLock>, - + // Performance tracking compilation_queue: Mutex>, background_compilation: AtomicBool, - + // Validation and debugging validation_enabled: AtomicBool, debug_info_enabled: AtomicBool, @@ -166,32 +169,32 @@ impl ShaderManager { let _ = tx.send(event); } })?; - + Ok(Self { device, shader_cache: RwLock::new(HashMap::new()), source_cache: RwLock::new(HashMap::new()), dependency_graph: RwLock::new(HashMap::new()), compilation_stats: RwLock::new(CompilationStats::default()), - + file_watcher: Arc::new(Mutex::new(Some(watcher))), hot_reload_enabled: AtomicBool::new(true), hot_reload_receiver: Arc::new(Mutex::new(Some(rx))), watched_directories: RwLock::new(HashSet::new()), - + include_directories: RwLock::new(Vec::new()), global_macros: RwLock::new(Vec::new()), preprocessor_cache: RwLock::new(HashMap::new()), - + compilation_queue: Mutex::new(Vec::new()), background_compilation: AtomicBool::new(true), - + validation_enabled: AtomicBool::new(true), debug_info_enabled: AtomicBool::new(false), optimization_enabled: AtomicBool::new(true), }) } - + /// Load and compile a shader #[instrument(skip(self))] pub fn load_shader( @@ -201,10 +204,10 @@ impl ShaderManager { variant: ShaderVariant, ) -> Result> { let path = path.as_ref().to_path_buf(); - + // Load source if not cached let source = self.load_source(&path, stage)?; - + // Check cache first let cache_key = (source.content_hash, variant.clone()); if let Some(cached) = self.shader_cache.read().get(&cache_key) { @@ -214,19 +217,21 @@ impl ShaderManager { *cached.last_used.write() = Instant::now(); return Ok(cached.clone()); } - + // Compile shader let compiled = self.compile_shader(&source, variant)?; - + // Cache the result - self.shader_cache.write().insert(cache_key, compiled.clone()); - + self.shader_cache + .write() + .insert(cache_key, compiled.clone()); + let mut stats = self.compilation_stats.write(); stats.cache_misses += 1; - + Ok(compiled) } - + /// Load shader source from file fn load_source(&self, path: &Path, stage: ShaderStage) -> Result> { // Check source cache @@ -237,25 +242,25 @@ impl ShaderManager { return Ok(cached.clone()); } } - + // Read file content let content = fs::read_to_string(path) .with_context(|| format!("Failed to read shader file: {}", path.display()))?; - + // Detect language let language = self.detect_shader_language(path, &content)?; - + // Preprocess shader let processed_content = self.preprocess_shader(&content, path)?; - + // Calculate content hash let mut hasher = Sha256::new(); hasher.update(&processed_content); let content_hash: [u8; 32] = hasher.finalize().into(); - + // Extract dependencies let (includes, dependencies) = self.extract_dependencies(&processed_content, path)?; - + let source = Arc::new(ShaderSource { path: path.to_path_buf(), content: processed_content, @@ -266,21 +271,23 @@ impl ShaderManager { last_modified: fs::metadata(path)?.modified()?, content_hash, }); - + // Update dependency graph self.update_dependency_graph(path, dependencies); - + // Cache the source - self.source_cache.write().insert(path.to_path_buf(), source.clone()); - + self.source_cache + .write() + .insert(path.to_path_buf(), source.clone()); + // Watch for changes if hot-reload is enabled if self.hot_reload_enabled.load(Ordering::Relaxed) { self.watch_file(path)?; } - + Ok(source) } - + /// Detect shader language from file extension and content fn detect_shader_language(&self, path: &Path, content: &str) -> Result { if let Some(ext) = path.extension().and_then(|e| e.to_str()) { @@ -292,9 +299,12 @@ impl ShaderManager { _ => {} } } - + // Try to detect from content - if content.contains("@vertex") || content.contains("@fragment") || content.contains("@compute") { + if content.contains("@vertex") + || content.contains("@fragment") + || content.contains("@compute") + { Ok(ShaderLanguage::WGSL) } else if content.contains("#version") || content.contains("gl_") { Ok(ShaderLanguage::GLSL) @@ -305,69 +315,70 @@ impl ShaderManager { Ok(ShaderLanguage::WGSL) } } - + /// Preprocess shader with includes and macros fn preprocess_shader(&self, content: &str, base_path: &Path) -> Result { let mut processed = content.to_string(); - + // Apply global macros for macro_def in self.global_macros.read().iter() { let replacement = match ¯o_def.value { Some(value) => format!("#define {} {}", macro_def.name, value), None => format!("#define {}", macro_def.name), }; - + let pattern = format!(r"#define\s+{}\s*.*", regex::escape(¯o_def.name)); let re = Regex::new(&pattern)?; processed = re.replace_all(&processed, replacement.as_str()).to_string(); } - + // Process includes processed = self.process_includes(&processed, base_path)?; - + Ok(processed) } - + /// Process #include directives fn process_includes(&self, content: &str, base_path: &Path) -> Result { let include_re = Regex::new(r#"#include\s+"([^"]+)""#)?; let mut processed = content.to_string(); let mut included_files = HashSet::new(); - + // Recursive include processing loop { let mut found_include = false; - + for cap in include_re.captures_iter(&processed.clone()) { let include_path = &cap[1]; let full_path = self.resolve_include_path(include_path, base_path)?; - + if included_files.contains(&full_path) { // Avoid circular includes continue; } - - let include_content = fs::read_to_string(&full_path) - .with_context(|| format!("Failed to read include file: {}", full_path.display()))?; - + + let include_content = fs::read_to_string(&full_path).with_context(|| { + format!("Failed to read include file: {}", full_path.display()) + })?; + processed = processed.replace(&cap[0], &include_content); included_files.insert(full_path); found_include = true; break; } - + if !found_include { break; } } - + Ok(processed) } - + /// Resolve include path relative to base path and include directories fn resolve_include_path(&self, include_path: &str, base_path: &Path) -> Result { let include_path = Path::new(include_path); - + // Try relative to current file if let Some(parent) = base_path.parent() { let full_path = parent.join(include_path); @@ -375,7 +386,7 @@ impl ShaderManager { return Ok(full_path); } } - + // Try include directories for include_dir in self.include_directories.read().iter() { let full_path = include_dir.join(include_path); @@ -383,16 +394,20 @@ impl ShaderManager { return Ok(full_path); } } - + bail!("Include file not found: {}", include_path.display()); } - + /// Extract shader dependencies from content - fn extract_dependencies(&self, content: &str, base_path: &Path) -> Result<(HashSet, HashSet)> { + fn extract_dependencies( + &self, + content: &str, + base_path: &Path, + ) -> Result<(HashSet, HashSet)> { let include_re = Regex::new(r#"#include\s+"([^"]+)""#)?; let mut includes = HashSet::new(); let mut dependencies = HashSet::new(); - + for cap in include_re.captures_iter(content) { let include_path = &cap[1]; if let Ok(full_path) = self.resolve_include_path(include_path, base_path) { @@ -400,45 +415,54 @@ impl ShaderManager { dependencies.insert(full_path); } } - + Ok((includes, dependencies)) } - + /// Update dependency graph fn update_dependency_graph(&self, path: &Path, dependencies: HashSet) { let mut graph = self.dependency_graph.write(); - + // Update current node - let node = graph.entry(path.to_path_buf()).or_insert_with(|| DependencyNode { - path: path.to_path_buf(), - dependents: HashSet::new(), - dependencies: HashSet::new(), - last_modified: SystemTime::now(), - }); - + let node = graph + .entry(path.to_path_buf()) + .or_insert_with(|| DependencyNode { + path: path.to_path_buf(), + dependents: HashSet::new(), + dependencies: HashSet::new(), + last_modified: SystemTime::now(), + }); + node.dependencies = dependencies.clone(); - node.last_modified = fs::metadata(path).ok() + node.last_modified = fs::metadata(path) + .ok() .and_then(|m| m.modified().ok()) .unwrap_or(SystemTime::now()); - + // Update dependent nodes for dep_path in dependencies { - let dep_node = graph.entry(dep_path.clone()).or_insert_with(|| DependencyNode { - path: dep_path.clone(), - dependents: HashSet::new(), - dependencies: HashSet::new(), - last_modified: SystemTime::now(), - }); - + let dep_node = graph + .entry(dep_path.clone()) + .or_insert_with(|| DependencyNode { + path: dep_path.clone(), + dependents: HashSet::new(), + dependencies: HashSet::new(), + last_modified: SystemTime::now(), + }); + dep_node.dependents.insert(path.to_path_buf()); } } - + /// Compile shader with variant #[instrument(skip(self, source))] - fn compile_shader(&self, source: &ShaderSource, variant: ShaderVariant) -> Result> { + fn compile_shader( + &self, + source: &ShaderSource, + variant: ShaderVariant, + ) -> Result> { let start_time = Instant::now(); - + // Apply variant macros let mut shader_source = source.content.clone(); for macro_def in &variant.macros { @@ -448,24 +472,24 @@ impl ShaderManager { }; shader_source = format!("{}{}", definition, shader_source); } - + // Create shader module descriptor let descriptor = ShaderModuleDescriptor { label: Some(&format!("Shader-{}", source.path.display())), source: wgpu::ShaderSource::Wgsl(shader_source.clone().into()), }; - + // Compile shader let module = self.device.device.create_shader_module(descriptor); - + let compilation_time = start_time.elapsed(); - + // Validate if enabled let mut validation_errors = Vec::new(); if self.validation_enabled.load(Ordering::Relaxed) { validation_errors = self.validate_shader(&module, &source)?; } - + let compiled = Arc::new(CompiledShader { module, source_hash: source.content_hash, @@ -477,7 +501,7 @@ impl ShaderManager { usage_count: AtomicU64::new(1), last_used: RwLock::new(Instant::now()), }); - + // Update statistics let mut stats = self.compilation_stats.write(); stats.total_compilations += 1; @@ -487,24 +511,29 @@ impl ShaderManager { stats.failed_compilations += 1; } stats.total_compilation_time += compilation_time; - stats.average_compilation_time = stats.total_compilation_time / stats.total_compilations as u32; - + stats.average_compilation_time = + stats.total_compilation_time / stats.total_compilations as u32; + info!( "Compiled shader: {} in {:?}", source.path.display(), compilation_time ); - + Ok(compiled) } - + /// Validate compiled shader - fn validate_shader(&self, _module: &ShaderModule, _source: &ShaderSource) -> Result> { + fn validate_shader( + &self, + _module: &ShaderModule, + _source: &ShaderSource, + ) -> Result> { // Placeholder for shader validation - + Ok(Vec::new()) } - + /// Watch file for changes fn watch_file(&self, path: &Path) -> Result<()> { if let Some(parent) = path.parent() { @@ -518,11 +547,11 @@ impl ShaderManager { } Ok(()) } - + /// Process hot-reload events pub fn process_hot_reload_events(&self) -> Result> { let mut events = Vec::new(); - + if let Some(ref receiver) = *self.hot_reload_receiver.lock() { while let Ok(event) = receiver.try_recv() { match event.kind { @@ -553,49 +582,50 @@ impl ShaderManager { } } } - + Ok(events) } - + /// Check if file is a shader file fn is_shader_file(&self, path: &Path) -> bool { if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - matches!(ext.to_lowercase().as_str(), "wgsl" | "glsl" | "hlsl" | "vert" | "frag" | "comp") + matches!( + ext.to_lowercase().as_str(), + "wgsl" | "glsl" | "hlsl" | "vert" | "frag" | "comp" + ) } else { false } } - + /// Invalidate shader cache for a file fn invalidate_shader_cache(&self, path: &Path) { // Remove from source cache self.source_cache.write().remove(path); - + // Find and remove dependent shaders from compiled cache let dependents = self.find_dependents(path); let mut cache = self.shader_cache.write(); - - cache.retain(|_, compiled| { - !dependents.contains(&compiled.source_hash) - }); - + + cache.retain(|_, compiled| !dependents.contains(&compiled.source_hash)); + let mut stats = self.compilation_stats.write(); stats.hot_reloads += 1; - + info!("Invalidated shader cache for: {}", path.display()); } - + /// Remove shader from cache fn remove_from_cache(&self, path: &Path) { self.source_cache.write().remove(path); self.dependency_graph.write().remove(path); } - + /// Find all shaders dependent on a file fn find_dependents(&self, path: &Path) -> HashSet<[u8; 32]> { let mut dependents = HashSet::new(); let graph = self.dependency_graph.read(); - + if let Some(node) = graph.get(path) { for dependent_path in &node.dependents { if let Some(source) = self.source_cache.read().get(dependent_path) { @@ -603,15 +633,17 @@ impl ShaderManager { } } } - + dependents } - + /// Add include directory pub fn add_include_directory(&self, path: impl AsRef) { - self.include_directories.write().push(path.as_ref().to_path_buf()); + self.include_directories + .write() + .push(path.as_ref().to_path_buf()); } - + /// Add global macro pub fn add_global_macro(&self, name: impl Into, value: Option) { self.global_macros.write().push(ShaderMacro { @@ -619,55 +651,59 @@ impl ShaderManager { value, }); } - + /// Enable or disable hot-reload pub fn set_hot_reload_enabled(&self, enabled: bool) { self.hot_reload_enabled.store(enabled, Ordering::Relaxed); } - + /// Enable or disable validation pub fn set_validation_enabled(&self, enabled: bool) { self.validation_enabled.store(enabled, Ordering::Relaxed); } - + /// Enable or disable optimization pub fn set_optimization_enabled(&self, enabled: bool) { self.optimization_enabled.store(enabled, Ordering::Relaxed); } - + /// Initialize the shader manager (placeholder for integration) pub fn initialize(&self) -> Result<()> { info!("Shader manager initialized"); Ok(()) } - + /// Check for shader reloads (integration method) pub fn check_for_reloads(&self) -> Result<()> { let _events = self.process_hot_reload_events()?; Ok(()) } - + /// Get compilation statistics pub fn get_stats(&self) -> CompilationStats { self.compilation_stats.read().clone() } - + /// Clear all caches pub fn clear_caches(&self) { self.shader_cache.write().clear(); self.source_cache.write().clear(); self.preprocessor_cache.write().clear(); - + info!("Cleared all shader caches"); } - + /// Get cache statistics pub fn get_cache_stats(&self) -> (usize, usize, usize) { let shader_cache_size = self.shader_cache.read().len(); let source_cache_size = self.source_cache.read().len(); let preprocessor_cache_size = self.preprocessor_cache.read().len(); - - (shader_cache_size, source_cache_size, preprocessor_cache_size) + + ( + shader_cache_size, + source_cache_size, + preprocessor_cache_size, + ) } } @@ -680,40 +716,43 @@ impl Drop for ShaderManager { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_shader_macro_creation() { let macro_def = ShaderMacro { name: "MAX_LIGHTS".to_string(), value: Some("16".to_string()), }; - + assert_eq!(macro_def.name, "MAX_LIGHTS"); assert_eq!(macro_def.value, Some("16".to_string())); } - + #[test] fn test_shader_variant_equality() { let variant1 = ShaderVariant { - macros: vec![ShaderMacro { name: "TEST".to_string(), value: None }], + macros: vec![ShaderMacro { + name: "TEST".to_string(), + value: None, + }], features: Vec::new(), optimization_level: 2, }; - + let variant2 = ShaderVariant { - macros: vec![ShaderMacro { name: "TEST".to_string(), value: None }], + macros: vec![ShaderMacro { + name: "TEST".to_string(), + value: None, + }], features: Vec::new(), optimization_level: 2, }; - + assert_eq!(variant1, variant2); } - + #[test] fn test_language_detection() { - assert_eq!( - ShaderLanguage::WGSL, - ShaderLanguage::WGSL - ); + assert_eq!(ShaderLanguage::WGSL, ShaderLanguage::WGSL); } -} \ No newline at end of file +} diff --git a/crates/strato-renderer/src/text.rs b/crates/strato-renderer/src/text.rs index ad1c7e5..eb19084 100644 --- a/crates/strato-renderer/src/text.rs +++ b/crates/strato-renderer/src/text.rs @@ -1,16 +1,15 @@ //! Text rendering with cosmic-text +use crate::glyph_atlas::GlyphAtlasManager; +use crate::vertex::{TextVertex, Vertex}; use cosmic_text::{ - Attrs, Buffer, Family, FontSystem, Metrics, Shaping, - Weight, Wrap, SwashCache, CacheKey, + Attrs, Buffer, CacheKey, Family, FontSystem, Metrics, Shaping, SwashCache, Weight, Wrap, }; -use std::sync::{Arc, Mutex}; -use std::collections::HashMap; -use parking_lot::RwLock; use dashmap::DashMap; use image::{DynamicImage, ImageBuffer, Rgba}; -use crate::glyph_atlas::{GlyphAtlasManager}; -use crate::vertex::{Vertex, TextVertex}; +use parking_lot::RwLock; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use strato_core::types::{Color, Point, Size}; /// Font wrapper @@ -64,16 +63,16 @@ impl Default for Font { // Use platform-specific default fonts #[cfg(target_os = "windows")] let default_family = "Segoe UI"; - + #[cfg(target_os = "macos")] let default_family = "San Francisco"; - + #[cfg(target_os = "linux")] let default_family = "Ubuntu"; - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let default_family = "sans-serif"; - + Self::new(default_family, 16.0) } } @@ -142,81 +141,101 @@ impl TextRenderer { let mut font_system = self.font_system.write(); let mut glyph_cache = self.glyph_cache.write(); let mut glyph_atlas = self.glyph_atlas_manager.write(); - + // Create buffer for layout let metrics = Metrics::new(font.size, font.size * 1.2); let mut buffer = Buffer::new(&mut font_system, metrics); buffer.set_text(&mut font_system, text, font.to_attrs(), Shaping::Advanced); - + if let Some(width) = max_width { buffer.set_wrap(&mut font_system, Wrap::Word); buffer.set_size(&mut font_system, Some(width), Some(f32::MAX)); } else { buffer.set_size(&mut font_system, None, Some(f32::MAX)); } - + buffer.shape_until_scroll(&mut font_system, false); - + let start_x = position.x; let start_y = position.y; - + // Iterate over layout runs for run in buffer.layout_runs() { for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((start_x, start_y + run.line_y), 1.0); - + // Get texture coordinates from atlas if let Some((_atlas_index, glyph_info)) = glyph_atlas.get_or_create_glyph( &mut font_system, &mut glyph_cache.cache, - physical_glyph.cache_key + physical_glyph.cache_key, ) { let glyph_x = physical_glyph.x as f32; let glyph_y = physical_glyph.y as f32; let glyph_w = glyph_info.size.0 as f32; let glyph_h = glyph_info.size.1 as f32; - + let (u0, v0, u1, v1) = glyph_info.uv_rect; - + // Create quad vertices for this glyph vertices.extend_from_slice(&[ - TextVertex::new([glyph_x, glyph_y, 0.0], [u0, v0], [color.r, color.g, color.b, color.a], 0), - TextVertex::new([glyph_x + glyph_w, glyph_y, 0.0], [u1, v0], [color.r, color.g, color.b, color.a], 0), - TextVertex::new([glyph_x + glyph_w, glyph_y + glyph_h, 0.0], [u1, v1], [color.r, color.g, color.b, color.a], 0), - TextVertex::new([glyph_x, glyph_y + glyph_h, 0.0], [u0, v1], [color.r, color.g, color.b, color.a], 0), + TextVertex::new( + [glyph_x, glyph_y, 0.0], + [u0, v0], + [color.r, color.g, color.b, color.a], + 0, + ), + TextVertex::new( + [glyph_x + glyph_w, glyph_y, 0.0], + [u1, v0], + [color.r, color.g, color.b, color.a], + 0, + ), + TextVertex::new( + [glyph_x + glyph_w, glyph_y + glyph_h, 0.0], + [u1, v1], + [color.r, color.g, color.b, color.a], + 0, + ), + TextVertex::new( + [glyph_x, glyph_y + glyph_h, 0.0], + [u0, v1], + [color.r, color.g, color.b, color.a], + 0, + ), ]); } } } - + vertices } /// Measure text dimensions pub fn measure_text(&self, text: &str, font: &Font, max_width: Option) -> Size { let mut font_system = self.font_system.write(); - + // Create buffer for measurement let metrics = Metrics::new(font.size, font.size * 1.2); let mut buffer = Buffer::new(&mut font_system, metrics); buffer.set_text(&mut font_system, text, font.to_attrs(), Shaping::Advanced); - + if let Some(width) = max_width { buffer.set_wrap(&mut font_system, Wrap::Word); buffer.set_size(&mut font_system, Some(width), Some(f32::MAX)); } - + buffer.shape_until_scroll(&mut font_system, false); - + let mut max_width: f32 = 0.0; let mut total_height = 0.0; - + for run in buffer.layout_runs() { let line_width = run.glyphs.iter().map(|g| g.w).sum::(); max_width = max_width.max(line_width); total_height += run.line_height; } - + Size::new(max_width, total_height) } @@ -225,7 +244,7 @@ impl TextRenderer { fn hash_text(text: &str, font: &Font) -> u64 { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; - + let mut hasher = DefaultHasher::new(); text.hash(&mut hasher); font.size.to_bits().hash(&mut hasher); @@ -249,32 +268,32 @@ impl TextRenderer { ) -> DynamicImage { let mut font_system = self.font_system.write(); let mut swash_cache = SwashCache::new(); - + // Create buffer for text layout let metrics = Metrics::new(font.size, font.size * 1.2); let mut buffer = Buffer::new(&mut font_system, metrics); buffer.set_text(&mut font_system, text, font.to_attrs(), Shaping::Advanced); - + if let Some(width) = max_width { buffer.set_wrap(&mut font_system, cosmic_text::Wrap::Word); buffer.set_size(&mut font_system, Some(width), Some(f32::MAX)); } - + buffer.shape_until_scroll(&mut font_system, false); - + // Calculate dimensions let size = self.measure_text(text, font, max_width); let width = size.width.ceil().max(1.0) as u32; let height = size.height.ceil().max(1.0) as u32; - + // Create pixel buffer let mut img = ImageBuffer::, Vec>::new(width, height); - + // Render each glyph for run in buffer.layout_runs() { for glyph in run.glyphs { let physical_glyph = glyph.physical((0.0, 0.0), 1.0); - + swash_cache.with_pixels( &mut font_system, physical_glyph.cache_key, @@ -282,7 +301,7 @@ impl TextRenderer { |x, y, color| { let px = (physical_glyph.x as i32 + x) as u32; let py = (physical_glyph.y as i32 + y) as u32; - + if px < width && py < height { let pixel = img.get_pixel_mut(px, py); pixel[0] = color.r(); @@ -294,7 +313,7 @@ impl TextRenderer { ); } } - + DynamicImage::ImageRgba8(img) } } diff --git a/crates/strato-renderer/src/texture.rs b/crates/strato-renderer/src/texture.rs index 6cec4bf..c17a513 100644 --- a/crates/strato-renderer/src/texture.rs +++ b/crates/strato-renderer/src/texture.rs @@ -1,8 +1,8 @@ //! Texture management and atlas -use std::sync::Arc; use dashmap::DashMap; use image::{DynamicImage, ImageBuffer, Rgba}; +use std::sync::Arc; /// Texture wrapper pub struct Texture { @@ -17,7 +17,7 @@ impl Texture { pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, image: &DynamicImage) -> Self { let rgba = image.to_rgba8(); let size = (image.width(), image.height()); - + let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Texture"), size: wgpu::Extent3d { @@ -54,7 +54,7 @@ impl Texture { ); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, @@ -127,9 +127,8 @@ pub struct TexCoords { impl TextureAtlas { /// Create a new texture atlas pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, size: (u32, u32)) -> Self { - let empty_image = ImageBuffer::, _>::from_fn(size.0, size.1, |_, _| { - Rgba([0, 0, 0, 0]) - }); + let empty_image = + ImageBuffer::, _>::from_fn(size.0, size.1, |_, _| Rgba([0, 0, 0, 0])); let image = DynamicImage::ImageRgba8(empty_image); let texture = Arc::new(Texture::new(device, queue, &image)); @@ -150,7 +149,7 @@ impl TextureAtlas { image: &DynamicImage, ) -> Option { let img_size = (image.width(), image.height()); - + let mut next_pos = self.next_position.lock(); let mut row_height = self.row_height.lock(); diff --git a/crates/strato-renderer/src/vertex.rs b/crates/strato-renderer/src/vertex.rs index b154c83..095a6b9 100644 --- a/crates/strato-renderer/src/vertex.rs +++ b/crates/strato-renderer/src/vertex.rs @@ -1,16 +1,16 @@ //! Vertex data structures and layouts for wgpu rendering -use wgpu::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode, BufferAddress}; +use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode}; /// Vertex data for UI rendering #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vertex { - pub position: [f32; 2], // Changed from 3D to 2D to match shader + pub position: [f32; 2], // Changed from 3D to 2D to match shader pub color: [f32; 4], - pub uv: [f32; 2], // Renamed from tex_coords to match shader - pub params: [f32; 4], // Added params field to match shader - pub flags: u32, // For different rendering modes (solid, textured, etc.) + pub uv: [f32; 2], // Renamed from tex_coords to match shader + pub params: [f32; 4], // Added params field to match shader + pub flags: u32, // For different rendering modes (solid, textured, etc.) } impl Vertex { @@ -100,7 +100,12 @@ pub struct TextVertex { impl TextVertex { /// Create a new text vertex - pub fn new(position: [f32; 3], tex_coords: [f32; 2], color: [f32; 4], glyph_index: u32) -> Self { + pub fn new( + position: [f32; 3], + tex_coords: [f32; 2], + color: [f32; 4], + glyph_index: u32, + ) -> Self { Self { position, tex_coords, @@ -157,10 +162,10 @@ impl VertexBuilder { color: [f32; 4], ) -> (Vec, Vec) { let vertices = vec![ - Vertex::solid([x, y], color), // Top-left - Vertex::solid([x + width, y], color), // Top-right - Vertex::solid([x + width, y + height], color), // Bottom-right - Vertex::solid([x, y + height], color), // Bottom-left + Vertex::solid([x, y], color), // Top-left + Vertex::solid([x + width, y], color), // Top-right + Vertex::solid([x + width, y + height], color), // Bottom-right + Vertex::solid([x, y + height], color), // Bottom-left ]; let indices = vec![0, 1, 2, 2, 3, 0]; @@ -177,10 +182,10 @@ impl VertexBuilder { color: [f32; 4], ) -> (Vec, Vec) { let vertices = vec![ - Vertex::textured([x, y], [0.0, 0.0], color), // Top-left - Vertex::textured([x + width, y], [1.0, 0.0], color), // Top-right - Vertex::textured([x + width, y + height], [1.0, 1.0], color), // Bottom-right - Vertex::textured([x, y + height], [0.0, 1.0], color), // Bottom-left + Vertex::textured([x, y], [0.0, 0.0], color), // Top-left + Vertex::textured([x + width, y], [1.0, 0.0], color), // Top-right + Vertex::textured([x + width, y + height], [1.0, 1.0], color), // Bottom-right + Vertex::textured([x, y + height], [0.0, 1.0], color), // Bottom-left ]; let indices = vec![0, 1, 2, 2, 3, 0]; @@ -237,8 +242,6 @@ impl VertexBuilder { // Flag: 3 = FLAG_TYPE_ROUNDED_RECT let flags = 3; - - vertices.push(Vertex { position: [x, y], color, @@ -287,12 +290,15 @@ impl VertexBuilder { // Create the four border lines let half_thickness = thickness / 2.0; - + // Top line let (top_verts, top_indices) = Self::line( - x + radius, y - half_thickness, - x + width - radius, y - half_thickness, - thickness, color + x + radius, + y - half_thickness, + x + width - radius, + y - half_thickness, + thickness, + color, ); vertices.extend(top_verts); indices.extend(top_indices); @@ -300,9 +306,12 @@ impl VertexBuilder { // Right line let offset = vertices.len() as u16; let (right_verts, right_indices) = Self::line( - x + width + half_thickness, y + radius, - x + width + half_thickness, y + height - radius, - thickness, color + x + width + half_thickness, + y + radius, + x + width + half_thickness, + y + height - radius, + thickness, + color, ); vertices.extend(right_verts); indices.extend(right_indices.iter().map(|&i| i + offset)); @@ -310,9 +319,12 @@ impl VertexBuilder { // Bottom line let offset = vertices.len() as u16; let (bottom_verts, bottom_indices) = Self::line( - x + width - radius, y + height + half_thickness, - x + radius, y + height + half_thickness, - thickness, color + x + width - radius, + y + height + half_thickness, + x + radius, + y + height + half_thickness, + thickness, + color, ); vertices.extend(bottom_verts); indices.extend(bottom_indices.iter().map(|&i| i + offset)); @@ -320,34 +332,39 @@ impl VertexBuilder { // Left line let offset = vertices.len() as u16; let (left_verts, left_indices) = Self::line( - x - half_thickness, y + height - radius, - x - half_thickness, y + radius, - thickness, color + x - half_thickness, + y + height - radius, + x - half_thickness, + y + radius, + thickness, + color, ); vertices.extend(left_verts); indices.extend(left_indices.iter().map(|&i| i + offset)); // Add rounded corners (outline arcs) let corners = [ - (x + radius, y + radius), // Top-left - (x + width - radius, y + radius), // Top-right - (x + width - radius, y + height - radius), // Bottom-right - (x + radius, y + height - radius), // Bottom-left + (x + radius, y + radius), // Top-left + (x + width - radius, y + radius), // Top-right + (x + width - radius, y + height - radius), // Bottom-right + (x + radius, y + height - radius), // Bottom-left ]; for (i, &(cx, cy)) in corners.iter().enumerate() { let start_angle = (i as f32) * std::f32::consts::PI / 2.0 + std::f32::consts::PI; - + // Create arc outline using multiple line segments for j in 0..corner_segments { - let angle1 = start_angle + (j as f32) * (std::f32::consts::PI / 2.0) / (corner_segments as f32); - let angle2 = start_angle + ((j + 1) as f32) * (std::f32::consts::PI / 2.0) / (corner_segments as f32); - + let angle1 = start_angle + + (j as f32) * (std::f32::consts::PI / 2.0) / (corner_segments as f32); + let angle2 = start_angle + + ((j + 1) as f32) * (std::f32::consts::PI / 2.0) / (corner_segments as f32); + let x1 = cx + radius * angle1.cos(); let y1 = cy + radius * angle1.sin(); let x2 = cx + radius * angle2.cos(); let y2 = cy + radius * angle2.sin(); - + let offset = vertices.len() as u16; let (arc_verts, arc_indices) = Self::line(x1, y1, x2, y2, thickness, color); vertices.extend(arc_verts); @@ -402,7 +419,7 @@ impl VertexBuilder { let dx = end_x - start_x; let dy = end_y - start_y; let length = (dx * dx + dy * dy).sqrt(); - + if length == 0.0 { return (Vec::new(), Vec::new()); } @@ -410,14 +427,26 @@ impl VertexBuilder { // Normalize and get perpendicular vector let nx = -dy / length; let ny = dx / length; - + let half_thickness = thickness * 0.5; - + let vertices = vec![ - Vertex::solid([start_x + nx * half_thickness, start_y + ny * half_thickness], color), - Vertex::solid([start_x - nx * half_thickness, start_y - ny * half_thickness], color), - Vertex::solid([end_x - nx * half_thickness, end_y - ny * half_thickness], color), - Vertex::solid([end_x + nx * half_thickness, end_y + ny * half_thickness], color), + Vertex::solid( + [start_x + nx * half_thickness, start_y + ny * half_thickness], + color, + ), + Vertex::solid( + [start_x - nx * half_thickness, start_y - ny * half_thickness], + color, + ), + Vertex::solid( + [end_x - nx * half_thickness, end_y - ny * half_thickness], + color, + ), + Vertex::solid( + [end_x + nx * half_thickness, end_y + ny * half_thickness], + color, + ), ]; let indices = vec![0, 1, 2, 2, 3, 0]; @@ -454,7 +483,8 @@ mod tests { #[test] fn test_rectangle_builder() { - let (vertices, indices) = VertexBuilder::rectangle(0.0, 0.0, 100.0, 50.0, [1.0, 0.0, 0.0, 1.0]); + let (vertices, indices) = + VertexBuilder::rectangle(0.0, 0.0, 100.0, 50.0, [1.0, 0.0, 0.0, 1.0]); assert_eq!(vertices.len(), 4); assert_eq!(indices.len(), 6); assert_eq!(vertices[0].position, [0.0, 0.0]); @@ -467,5 +497,4 @@ mod tests { assert_eq!(vertices.len(), 9); // Center + 8 segments assert_eq!(indices.len(), 24); // 8 triangles * 3 indices } - } diff --git a/crates/strato-widgets/src/animation.rs b/crates/strato-widgets/src/animation.rs index 5152b4c..6469877 100644 --- a/crates/strato-widgets/src/animation.rs +++ b/crates/strato-widgets/src/animation.rs @@ -19,7 +19,13 @@ impl Curve { Curve::Linear => t, Curve::EaseIn => t * t, Curve::EaseOut => t * (2.0 - t), - Curve::EaseInOut => if t < 0.5 { 2.0 * t * t } else { -1.0 + (4.0 - 2.0 * t) * t }, + Curve::EaseInOut => { + if t < 0.5 { + 2.0 * t * t + } else { + -1.0 + (4.0 - 2.0 * t) * t + } + } } } } @@ -76,13 +82,13 @@ impl AnimationController { let elapsed = start.elapsed().as_secs_f32(); let duration = self.duration.as_secs_f32(); - + if duration == 0.0 { return 1.0; } let raw_t = elapsed / duration; - + let t = if self.is_repeating { let cycle = raw_t % 2.0; if cycle > 1.0 { @@ -205,7 +211,7 @@ impl Timeline { if self.status == AnimationStatus::Playing { if let Some(start) = self.start_time { let current_elapsed = self.elapsed + start.elapsed().mul_f32(self.speed); - + let mut all_finished = true; for anim in &mut self.animations { anim.update(current_elapsed); @@ -220,7 +226,7 @@ impl Timeline { } } } - + pub fn reset(&mut self) { self.status = AnimationStatus::Paused; self.start_time = None; @@ -239,8 +245,6 @@ pub trait Animation: std::fmt::Debug { fn duration(&self) -> Duration; } - - // Re-implementing KeyframeAnimation properly with state for is_finished /// Animation that interpolates a value over time using a Signal target #[derive(Debug)] @@ -252,7 +256,7 @@ pub struct KeyframeAnimation { } impl KeyframeAnimation { - pub fn new(duration: Duration, tween: Tween, target: strato_core::state::Signal) -> Self { + pub fn new(duration: Duration, tween: Tween, target: strato_core::state::Signal) -> Self { Self { controller: AnimationController::new(duration), tween, @@ -260,7 +264,7 @@ impl KeyframeAnimation { finished: false, } } - + pub fn with_curve(mut self, curve: Curve) -> Self { self.controller = self.controller.with_curve(curve); self @@ -270,40 +274,39 @@ impl KeyframeAnimation { impl Animation for KeyframeAnimation { fn update(&mut self, elapsed: Duration) { let d_secs = self.controller.duration.as_secs_f32(); - if d_secs == 0.0 { + if d_secs == 0.0 { self.finished = true; - return; + return; } let t_secs = elapsed.as_secs_f32(); let raw_t = t_secs / d_secs; - + self.finished = raw_t >= 1.0; - + let t = raw_t.clamp(0.0, 1.0); let curved_t = self.controller.curve.transform(t); let value = self.tween.transform(curved_t); - + self.target.set(value); } - + fn is_finished(&self) -> bool { self.finished } - + fn reset(&mut self) { self.finished = false; // Optionally reset value to start? // let value = self.tween.transform(0.0); // self.target.set(value); } - + fn duration(&self) -> Duration { self.controller.duration } } - /// Run animations in sequence #[derive(Debug)] pub struct Sequence { @@ -312,20 +315,18 @@ pub struct Sequence { impl Sequence { pub fn new(animations: Vec>) -> Self { - Self { - animations, - } + Self { animations } } } impl Animation for Sequence { fn update(&mut self, elapsed: Duration) { let mut time_so_far = Duration::ZERO; - + for anim in &mut self.animations { let duration = anim.duration(); let anim_end_time = time_so_far + duration; - + if elapsed >= anim_end_time { // Ensure this animation is in its final state anim.update(duration); @@ -336,13 +337,12 @@ impl Animation for Sequence { // Future anim.update(Duration::ZERO); } - + time_so_far += duration; } } fn is_finished(&self) -> bool { - if let Some(last) = self.animations.last() { last.is_finished() } else { @@ -369,9 +369,7 @@ pub struct Parallel { impl Parallel { pub fn new(animations: Vec>) -> Self { - Self { - animations - } + Self { animations } } } @@ -381,17 +379,17 @@ impl Animation for Parallel { anim.update(elapsed); } } - + fn is_finished(&self) -> bool { self.animations.iter().all(|a| a.is_finished()) } - + fn reset(&mut self) { for anim in &mut self.animations { anim.reset(); } } - + fn duration(&self) -> Duration { self.animations .iter() diff --git a/crates/strato-widgets/src/button.rs b/crates/strato-widgets/src/button.rs index 304a545..b587dff 100644 --- a/crates/strato-widgets/src/button.rs +++ b/crates/strato-widgets/src/button.rs @@ -1,31 +1,22 @@ //! Button widget implementation -//! +//! //! Provides interactive button components with various styles, states, and event handling. +use crate::control::{ControlRole, ControlState}; +use crate::widget::{generate_id, Widget, WidgetContext, WidgetId, WidgetState}; +use std::{any::Any, sync::Arc}; use strato_core::{ - layout::{Size, Constraints, Layout}, - types::Rect, + event::{Event, EventResult}, + layout::{Constraints, Layout, Size}, state::Signal, - theme::{Theme, Color}, + theme::{Color, Theme}, + types::Rect, types::{Point, Transform}, - event::{Event, EventResult}, }; -use strato_renderer::{ - vertex::VertexBuilder, - batch::RenderBatch, -}; -use crate::widget::{Widget, WidgetId, generate_id}; -use std::{sync::Arc, any::Any}; - -/// Button widget state -#[derive(Debug, Clone, PartialEq)] -pub enum ButtonState { - Normal, - Hovered, - Pressed, - Disabled, - Focused, -} +use strato_renderer::{batch::RenderBatch, vertex::VertexBuilder}; + +/// Button state is kept in sync with the shared widget state enum. +pub type ButtonState = WidgetState; /// Button style configuration #[derive(Debug, Clone)] @@ -133,12 +124,22 @@ impl ButtonStyle { } } +fn blend_colors(from: Color, to: Color, t: f32) -> Color { + let mix = |a: f32, b: f32| a + (b - a) * t; + Color::rgba( + mix(from.r, to.r), + mix(from.g, to.g), + mix(from.b, to.b), + mix(from.a, to.a), + ) +} + /// Button widget pub struct Button { id: WidgetId, text: String, style: ButtonStyle, - state: Signal, + control: ControlState, bounds: Signal, enabled: Signal, visible: Signal, @@ -153,12 +154,18 @@ impl std::fmt::Debug for Button { .field("id", &self.id) .field("text", &self.text) .field("style", &self.style) - .field("state", &self.state) + .field("state", &self.control) .field("bounds", &self.bounds) .field("enabled", &self.enabled) .field("visible", &self.visible) - .field("on_click", &self.on_click.as_ref().map(|_| "Fn() + Send + Sync")) - .field("on_hover", &self.on_hover.as_ref().map(|_| "Fn(bool) + Send + Sync")) + .field( + "on_click", + &self.on_click.as_ref().map(|_| "Fn() + Send + Sync"), + ) + .field( + "on_hover", + &self.on_hover.as_ref().map(|_| "Fn(bool) + Send + Sync"), + ) .field("theme", &self.theme) .finish() } @@ -167,11 +174,14 @@ impl std::fmt::Debug for Button { impl Button { /// Create a new button with text pub fn new(text: impl Into) -> Self { + let text_value = text.into(); + let mut control = ControlState::new(ControlRole::Button); + control.set_label(text_value.clone()); Self { id: generate_id(), - text: text.into(), + text: text_value, style: ButtonStyle::default(), - state: Signal::new(ButtonState::Normal), + control, bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), enabled: Signal::new(true), visible: Signal::new(true), @@ -238,6 +248,7 @@ impl Button { /// Set enabled state pub fn enabled(self, enabled: bool) -> Self { self.enabled.set(enabled); + self.control.set_disabled(!enabled); self } @@ -272,22 +283,36 @@ impl Button { /// Set button text pub fn set_text(&mut self, text: impl Into) { - self.text = text.into(); + let text = text.into(); + self.text = text.clone(); + self.control.set_label(text); + } + + /// Override the accessibility label without changing the visible text. + pub fn accessibility_label(mut self, label: impl Into) -> Self { + self.control.set_label(label); + self + } + + /// Provide an accessibility hint/description for assistive technologies. + pub fn accessibility_hint(mut self, hint: impl Into) -> Self { + self.control.set_hint(hint); + self } /// Get current state pub fn get_state(&self) -> ButtonState { - self.state.get() + self.control.state() } /// Set button state pub fn set_state(&self, state: ButtonState) { - self.state.set(state); + self.control.set_state(state); } /// Check if button is enabled pub fn is_enabled(&self) -> bool { - self.enabled.get() + self.enabled.get() && self.control.state() != ButtonState::Disabled } /// Check if button is visible @@ -298,7 +323,7 @@ impl Button { /// Handle mouse enter event pub fn on_mouse_enter(&self) { if self.is_enabled() && self.get_state() != ButtonState::Pressed { - self.set_state(ButtonState::Hovered); + self.control.hover(true); if let Some(ref handler) = self.on_hover { handler(true); } @@ -308,7 +333,7 @@ impl Button { /// Handle mouse leave event pub fn on_mouse_leave(&self) { if self.is_enabled() { - self.set_state(ButtonState::Normal); + self.control.hover(false); if let Some(ref handler) = self.on_hover { handler(false); } @@ -322,11 +347,7 @@ impl Button { } let bounds = self.bounds.get(); - if bounds.contains(point) { - self.set_state(ButtonState::Pressed); - return true; - } - false + self.control.press(point, bounds) } /// Handle mouse release event @@ -336,8 +357,7 @@ impl Button { } let bounds = self.bounds.get(); - if bounds.contains(point) && self.get_state() == ButtonState::Pressed { - self.set_state(ButtonState::Hovered); + if self.control.release(point, bounds) { if let Some(ref handler) = self.on_click { handler(); } @@ -366,7 +386,6 @@ impl Button { self.bounds.set(bounds); } - /// Apply theme to button pub fn apply_theme(&mut self, theme: &Theme) { // Update style based on theme @@ -480,21 +499,27 @@ mod tests { let primary = Button::new("Primary").primary(); let secondary = Button::new("Secondary").secondary(); let danger = Button::new("Danger").danger(); - + // Styles should be different - assert_ne!(primary.style.background_color, secondary.style.background_color); - assert_ne!(secondary.style.background_color, danger.style.background_color); + assert_ne!( + primary.style.background_color, + secondary.style.background_color + ); + assert_ne!( + secondary.style.background_color, + danger.style.background_color + ); } #[test] fn test_button_state_changes() { let button = Button::new("Test"); - + assert_eq!(button.get_state(), ButtonState::Normal); - + button.on_mouse_enter(); assert_eq!(button.get_state(), ButtonState::Hovered); - + button.on_mouse_leave(); assert_eq!(button.get_state(), ButtonState::Normal); } @@ -505,7 +530,7 @@ mod tests { .primary() .enabled(true) .build(); - + assert_eq!(button.text(), "Builder Test"); assert!(button.is_enabled()); } @@ -515,7 +540,7 @@ mod tests { let button = Button::new("Test"); let available = Size::new(200.0, 100.0); let size = button.calculate_size(available); - + assert!(size.width >= button.style.min_width); assert!(size.height >= button.style.min_height); assert!(size.width <= available.width); @@ -532,35 +557,53 @@ impl Widget for Button { fn layout(&mut self, constraints: Constraints) -> Size { let text_width = crate::text::measure_text_width(&self.text, self.style.font_size, 0.0); let text_height = self.style.font_size; - + let content_width = text_width + self.style.padding * 2.0; let content_height = text_height + self.style.padding * 2.0; - + let width = content_width.max(self.style.min_width); let height = content_height.max(self.style.min_height); - + // Respect constraints let width = width.min(constraints.max_width).max(constraints.min_width); - let height = height.min(constraints.max_height).max(constraints.min_height); - + let height = height + .min(constraints.max_height) + .max(constraints.min_height); + Size::new(width, height) } fn render(&self, batch: &mut RenderBatch, layout: Layout) { - let bounds = Rect::new(layout.position.x, layout.position.y, layout.size.width, layout.size.height); + let bounds = Rect::new( + layout.position.x, + layout.position.y, + layout.size.width, + layout.size.height, + ); self.bounds.set(bounds); - + if !self.is_visible() { return; } let state = self.get_state(); - let background_color = match state { + let target_color = match state { ButtonState::Normal => self.style.background_color, ButtonState::Hovered => self.style.hover_color, ButtonState::Pressed => self.style.pressed_color, ButtonState::Disabled => self.style.disabled_color, - ButtonState::Focused => self.style.hover_color, + ButtonState::Focused => { + blend_colors(self.style.background_color, self.style.hover_color, 0.35) + } + }; + let background_color = if matches!(state, ButtonState::Disabled) { + self.style.disabled_color + } else { + blend_colors( + self.style.background_color, + target_color, + self.control.interaction_factor(), + ) }; // Apply a subtle offset when pressed to give physical feedback @@ -604,110 +647,57 @@ impl Widget for Button { // Render text let text_x = draw_bounds.x + draw_bounds.width / 2.0; let text_y = draw_bounds.y + draw_bounds.height / 2.0 - self.style.font_size / 2.0; - + let mut text_color = self.style.text_color; + if matches!(state, ButtonState::Disabled) { + text_color.a *= 0.35; + } + batch.add_text_aligned( self.text.clone(), (text_x, text_y), - self.style.text_color.to_types_color(), + text_color.to_types_color(), self.style.font_size, 0.0, // Default letter spacing strato_core::text::TextAlign::Center, ); } + fn update(&mut self, ctx: &WidgetContext) { + self.control.update(ctx.delta_time); + } + fn handle_event(&mut self, event: &Event) -> EventResult { - // println!("Button '{}' handling event: {:?}", self.text, event); - match event { - Event::MouseMove(mouse_event) => { - let bounds = self.bounds.get(); - let point = strato_core::types::Point::new(mouse_event.position.x, mouse_event.position.y); - let is_hovered = bounds.contains(point); - - // Debug log for hover issues - if self.text == "Action" || self.text.contains("Button") { - // tracing::trace!("Button '{}' bounds: {:?}, mouse: {:?}, hovered: {}", self.text, bounds, point, is_hovered); - // println!("Button '{}' bounds: {:?}, mouse: {:?}, hovered: {}", self.text, bounds, point, is_hovered); - } + let previous_state = self.get_state(); + let bounds = self.bounds.get(); - if is_hovered { - if self.get_state() != ButtonState::Pressed { - self.state.set(ButtonState::Hovered); - } - if let Some(handler) = &self.on_hover { - handler(true); - } - return EventResult::Handled; - } else { - if self.get_state() == ButtonState::Hovered || self.get_state() == ButtonState::Pressed { - self.state.set(ButtonState::Normal); - if let Some(handler) = &self.on_hover { - handler(false); - } - } - } - } - Event::MouseDown(mouse_event) => { - if mouse_event.button == Some(strato_core::event::MouseButton::Left) { - let bounds = self.bounds.get(); - let point = strato_core::types::Point::new(mouse_event.position.x, mouse_event.position.y); - if bounds.contains(point) { - self.state.set(ButtonState::Pressed); - return EventResult::Handled; - } + // Pointer interactions and hover callbacks + if let EventResult::Handled = self.control.handle_pointer_event(event, bounds) { + if matches!(event, Event::MouseUp(_)) && matches!(previous_state, ButtonState::Pressed) + { + if let Some(handler) = &self.on_click { + handler(); } } - Event::MouseUp(mouse_event) => { - if mouse_event.button == Some(strato_core::event::MouseButton::Left) { - if self.get_state() == ButtonState::Pressed { - // Check if we are still hovered to decide state - let bounds = self.bounds.get(); - let point = strato_core::types::Point::new(mouse_event.position.x, mouse_event.position.y); - let is_hovered = bounds.contains(point); - - if is_hovered { - self.state.set(ButtonState::Hovered); - if let Some(handler) = &self.on_click { - handler(); - } - } else { - self.state.set(ButtonState::Normal); - } - return EventResult::Handled; - } + if let Event::MouseMove(mouse_event) = event { + let is_hovered = + bounds.contains(Point::new(mouse_event.position.x, mouse_event.position.y)); + if let Some(handler) = &self.on_hover { + handler(is_hovered); } } - Event::KeyDown(key_event) => { - // Simple focus check: if state is Focused or just handle if we decide to support implicit focus - // For now, let's assume if we receive the event, we might want to handle it if focused. - // But without focus system, we can't really know. - // However, users might want to trigger buttons with keys. - - if self.get_state() == ButtonState::Focused { - match key_event.key_code { - strato_core::event::KeyCode::Enter | strato_core::event::KeyCode::Space => { - self.state.set(ButtonState::Pressed); - return EventResult::Handled; - } - _ => {} - } - } - } - Event::KeyUp(key_event) => { - if self.get_state() == ButtonState::Pressed { - match key_event.key_code { - strato_core::event::KeyCode::Enter | strato_core::event::KeyCode::Space => { - self.state.set(ButtonState::Focused); - if let Some(handler) = &self.on_click { - handler(); - } - return EventResult::Handled; - } - _ => {} - } + return EventResult::Handled; + } + + // Keyboard accessibility + if let EventResult::Handled = self.control.handle_keyboard_activation(event) { + if matches!(event, Event::KeyUp(_)) { + if let Some(handler) = &self.on_click { + handler(); } } - _ => {} + return EventResult::Handled; } + EventResult::Ignored } @@ -724,7 +714,7 @@ impl Widget for Button { id: generate_id(), text: self.text.clone(), style: self.style.clone(), - state: Signal::new(self.state.get()), + control: self.control.clone(), bounds: Signal::new(self.bounds.get()), enabled: Signal::new(self.enabled.get()), visible: Signal::new(self.visible.get()), @@ -734,4 +724,3 @@ impl Widget for Button { }) } } - diff --git a/crates/strato-widgets/src/checkbox.rs b/crates/strato-widgets/src/checkbox.rs index cfb3eac..29efecd 100644 --- a/crates/strato-widgets/src/checkbox.rs +++ b/crates/strato-widgets/src/checkbox.rs @@ -1,13 +1,14 @@ //! Checkbox widget implementation for StratoUI +use crate::control::{ControlRole, ControlState}; +use crate::widget::{generate_id, Widget, WidgetContext, WidgetId, WidgetState}; use std::any::Any; -use crate::widget::{Widget, WidgetId, generate_id}; use strato_core::{ event::{Event, EventResult, MouseButton}, - layout::{Size, Constraints, Layout}, + layout::{Constraints, Layout, Size}, state::Signal, theme::Theme, - types::{Point, Rect, Color, Transform}, + types::{Color, Point, Rect, Transform}, vdom::VNode, }; use strato_renderer::batch::RenderBatch; @@ -22,6 +23,7 @@ pub struct Checkbox { size: f32, style: CheckboxStyle, bounds: Signal, + control: ControlState, } /// Styling options for checkbox @@ -43,18 +45,29 @@ impl Default for CheckboxStyle { size: 20.0, border_width: 2.0, border_radius: 4.0, - check_color: [1.0, 1.0, 1.0, 1.0], // White - border_color: [0.5, 0.5, 0.5, 1.0], // Gray + check_color: [1.0, 1.0, 1.0, 1.0], // White + border_color: [0.5, 0.5, 0.5, 1.0], // Gray background_color: [0.2, 0.6, 1.0, 1.0], // Blue - hover_color: [0.3, 0.7, 1.0, 1.0], // Light blue - disabled_color: [0.7, 0.7, 0.7, 1.0], // Light gray + hover_color: [0.3, 0.7, 1.0, 1.0], // Light blue + disabled_color: [0.7, 0.7, 0.7, 1.0], // Light gray } } } +fn color_from(values: [f32; 4]) -> Color { + Color::rgba(values[0], values[1], values[2], values[3]) +} + +fn blend_color(a: Color, b: Color, t: f32) -> Color { + let mix = |from: f32, to: f32| from + (to - from) * t; + Color::rgba(mix(a.r, b.r), mix(a.g, b.g), mix(a.b, b.b), mix(a.a, b.a)) +} + impl Checkbox { /// Create a new checkbox pub fn new() -> Self { + let mut control = ControlState::new(ControlRole::Checkbox); + control.set_toggled(false); Self { id: generate_id(), checked: Signal::new(false), @@ -63,24 +76,29 @@ impl Checkbox { size: 20.0, style: CheckboxStyle::default(), bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), + control, } } /// Set the checked state pub fn checked(mut self, checked: bool) -> Self { self.checked.set(checked); + self.control.set_toggled(checked); self } /// Set the label text pub fn label>(mut self, label: S) -> Self { - self.label = Some(label.into()); + let label = label.into(); + self.control.set_label(label.clone()); + self.label = Some(label); self } /// Set enabled state pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; + self.control.set_disabled(!enabled); self } @@ -108,13 +126,14 @@ impl Checkbox { } /// Toggle the checkbox state - pub fn toggle(&self) { + pub fn toggle(&mut self) { let current = self.checked.get(); self.checked.set(!current); + self.control.set_toggled(!current); } /// Handle click event - fn handle_click(&self) -> EventResult { + fn handle_click(&mut self) -> EventResult { if self.enabled { self.toggle(); EventResult::Handled @@ -127,7 +146,7 @@ impl Checkbox { fn create_checkbox_node(&self, theme: &Theme) -> VNode { let checked = self.checked.get(); let size = self.style.size; - + let background_color = if !self.enabled { self.style.disabled_color } else if checked { @@ -140,24 +159,45 @@ impl Checkbox { .attr("class", "checkbox") .attr("width", size.to_string()) .attr("height", size.to_string()) - .attr("background-color", format!("rgba({}, {}, {}, {})", - background_color[0], background_color[1], - background_color[2], background_color[3])) - .attr("border", format!("{}px solid rgba({}, {}, {}, {})", - self.style.border_width, - self.style.border_color[0], self.style.border_color[1], - self.style.border_color[2], self.style.border_color[3])) + .attr( + "background-color", + format!( + "rgba({}, {}, {}, {})", + background_color[0], + background_color[1], + background_color[2], + background_color[3] + ), + ) + .attr( + "border", + format!( + "{}px solid rgba({}, {}, {}, {})", + self.style.border_width, + self.style.border_color[0], + self.style.border_color[1], + self.style.border_color[2], + self.style.border_color[3] + ), + ) .attr("border-radius", format!("{}px", self.style.border_radius)); // Add checkmark if checked if checked { let checkmark = VNode::element("div") .attr("class", "checkmark") - .attr("color", format!("rgba({}, {}, {}, {})", - self.style.check_color[0], self.style.check_color[1], - self.style.check_color[2], self.style.check_color[3])) + .attr( + "color", + format!( + "rgba({}, {}, {}, {})", + self.style.check_color[0], + self.style.check_color[1], + self.style.check_color[2], + self.style.check_color[3] + ), + ) .children(vec![VNode::text("✓")]); - + checkbox = checkbox.children(vec![checkmark]); } @@ -184,56 +224,84 @@ impl Widget for Checkbox { } else { 0.0 }; - + let total_width = checkbox_size + label_width; let height = checkbox_size.max(20.0); // Minimum height for text - + constraints.constrain(Size::new(total_width, height)) } fn render(&self, batch: &mut RenderBatch, layout: Layout) { - let bounds = Rect::new(layout.position.x, layout.position.y, layout.size.width, layout.size.height); + let bounds = Rect::new( + layout.position.x, + layout.position.y, + layout.size.width, + layout.size.height, + ); self.bounds.set(bounds); - + // Draw checkbox background // Center vertically let box_y = bounds.y + (bounds.height - self.style.size) / 2.0; let box_rect = Rect::new(bounds.x, box_y, self.style.size, self.style.size); - - let bg_color = if self.is_checked() { - Color::rgba(self.style.background_color[0], self.style.background_color[1], self.style.background_color[2], self.style.background_color[3]) + let state = self.control.state(); + let base_color = if !self.enabled { + color_from(self.style.disabled_color) + } else if self.is_checked() { + color_from(self.style.background_color) } else { Color::WHITE }; - + + let hover_color = if self.enabled { + color_from(self.style.hover_color) + } else { + base_color + }; + + let target_color = match state { + WidgetState::Hovered | WidgetState::Pressed => hover_color, + WidgetState::Disabled => color_from(self.style.disabled_color), + _ => base_color, + }; + let bg_color = blend_color(base_color, target_color, self.control.interaction_factor()); + batch.add_rect(box_rect, bg_color, Transform::identity()); - + // Draw label if let Some(label) = &self.label { let text_x = bounds.x + self.style.size + 8.0; let text_y = bounds.y + bounds.height / 2.0 - 7.0; // approx center - batch.add_text(label.clone(), (text_x, text_y), Color::BLACK, 14.0, 0.0); + let mut label_color = Color::BLACK; + if !self.enabled { + label_color.a = 0.6; + } + batch.add_text(label.clone(), (text_x, text_y), label_color, 14.0, 0.0); } } + fn update(&mut self, ctx: &WidgetContext) { + self.control.update(ctx.delta_time); + } + fn handle_event(&mut self, event: &Event) -> EventResult { - match event { - Event::MouseDown(mouse_event) => { - if let Some(MouseButton::Left) = mouse_event.button { - let bounds = self.bounds.get(); - let point = Point::new(mouse_event.position.x, mouse_event.position.y); - if bounds.contains(point) { - self.handle_click(); - EventResult::Handled - } else { - EventResult::Ignored - } - } else { - EventResult::Ignored + if let EventResult::Handled = self.control.handle_pointer_event(event, self.bounds.get()) { + if matches!(event, Event::MouseUp(_)) { + if self.enabled { + self.handle_click(); } } - _ => EventResult::Ignored, + return EventResult::Handled; + } + + if let EventResult::Handled = self.control.handle_keyboard_activation(event) { + if matches!(event, Event::KeyUp(_)) && self.enabled { + self.handle_click(); + } + return EventResult::Handled; } + + EventResult::Ignored } fn as_any(&self) -> &dyn Any { @@ -262,6 +330,7 @@ pub struct RadioButton { enabled: bool, style: RadioStyle, bounds: Signal, + control: ControlState, } /// Styling options for radio button @@ -281,11 +350,11 @@ impl Default for RadioStyle { Self { size: 20.0, border_width: 2.0, - dot_color: [1.0, 1.0, 1.0, 1.0], // White - border_color: [0.5, 0.5, 0.5, 1.0], // Gray + dot_color: [1.0, 1.0, 1.0, 1.0], // White + border_color: [0.5, 0.5, 0.5, 1.0], // Gray background_color: [0.2, 0.6, 1.0, 1.0], // Blue - hover_color: [0.3, 0.7, 1.0, 1.0], // Light blue - disabled_color: [0.7, 0.7, 0.7, 1.0], // Light gray + hover_color: [0.3, 0.7, 1.0, 1.0], // Light blue + disabled_color: [0.7, 0.7, 0.7, 1.0], // Light gray } } } @@ -293,6 +362,8 @@ impl Default for RadioStyle { impl RadioButton { /// Create a new radio button pub fn new>(group: S, value: S) -> Self { + let mut control = ControlState::new(ControlRole::Radio); + control.set_toggled(false); Self { id: generate_id(), selected: Signal::new(false), @@ -302,24 +373,29 @@ impl RadioButton { enabled: true, style: RadioStyle::default(), bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), + control, } } /// Set the selected state pub fn selected(mut self, selected: bool) -> Self { self.selected.set(selected); + self.control.set_toggled(selected); self } /// Set the label text pub fn label>(mut self, label: S) -> Self { - self.label = Some(label.into()); + let label = label.into(); + self.control.set_label(label.clone()); + self.label = Some(label); self } /// Set enabled state pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; + self.control.set_disabled(!enabled); self } @@ -350,13 +426,15 @@ impl RadioButton { } /// Select this radio button - pub fn select(&self) { + pub fn select(&mut self) { self.selected.set(true); + self.control.set_toggled(true); } /// Deselect this radio button - pub fn deselect(&self) { + pub fn deselect(&mut self) { self.selected.set(false); + self.control.set_toggled(false); } } @@ -372,60 +450,94 @@ impl Widget for RadioButton { } else { 0.0 }; - + let total_width = radio_size + label_width; let height = radio_size.max(20.0); - + constraints.constrain(Size::new(total_width, height)) } fn render(&self, batch: &mut RenderBatch, layout: Layout) { - let bounds = Rect::new(layout.position.x, layout.position.y, layout.size.width, layout.size.height); + let bounds = Rect::new( + layout.position.x, + layout.position.y, + layout.size.width, + layout.size.height, + ); self.bounds.set(bounds); - + // Draw radio background (circle) let radio_y = bounds.y + (bounds.height - self.style.size) / 2.0; - let center = (bounds.x + self.style.size / 2.0, radio_y + self.style.size / 2.0); + let center = ( + bounds.x + self.style.size / 2.0, + radio_y + self.style.size / 2.0, + ); let radius = self.style.size / 2.0; - - let bg_color = if self.is_selected() { - Color::rgba(self.style.background_color[0], self.style.background_color[1], self.style.background_color[2], self.style.background_color[3]) + + let state = self.control.state(); + let base_color = if !self.enabled { + color_from(self.style.disabled_color) + } else if self.is_selected() { + color_from(self.style.background_color) } else { Color::WHITE }; - - batch.add_circle(center, radius, bg_color, 16, strato_core::types::Transform::default()); - + + let hover_color = if self.enabled { + color_from(self.style.hover_color) + } else { + base_color + }; + + let target_color = match state { + WidgetState::Hovered | WidgetState::Pressed => hover_color, + WidgetState::Disabled => color_from(self.style.disabled_color), + _ => base_color, + }; + let bg_color = blend_color(base_color, target_color, self.control.interaction_factor()); + + batch.add_circle( + center, + radius, + bg_color, + 16, + strato_core::types::Transform::default(), + ); + // Draw label if let Some(label) = &self.label { let text_x = bounds.x + self.style.size + 8.0; let text_y = bounds.y + bounds.height / 2.0 - 7.0; // approx center - batch.add_text(label.clone(), (text_x, text_y), Color::BLACK, 14.0, 0.0); + let mut label_color = Color::BLACK; + if !self.enabled { + label_color.a = 0.6; + } + batch.add_text(label.clone(), (text_x, text_y), label_color, 14.0, 0.0); } } + fn update(&mut self, ctx: &WidgetContext) { + self.control.update(ctx.delta_time); + } + fn handle_event(&mut self, event: &Event) -> EventResult { - match event { - Event::MouseDown(mouse_event) => { - if let Some(MouseButton::Left) = mouse_event.button { - let bounds = self.bounds.get(); - let point = Point::new(mouse_event.position.x, mouse_event.position.y); - if bounds.contains(point) { - if self.enabled { - self.select(); - EventResult::Handled - } else { - EventResult::Ignored - } - } else { - EventResult::Ignored - } - } else { - EventResult::Ignored + if let EventResult::Handled = self.control.handle_pointer_event(event, self.bounds.get()) { + if matches!(event, Event::MouseUp(_)) { + if self.enabled { + self.select(); } } - _ => EventResult::Ignored, + return EventResult::Handled; + } + + if let EventResult::Handled = self.control.handle_keyboard_activation(event) { + if matches!(event, Event::KeyUp(_)) && self.enabled { + self.select(); + } + return EventResult::Handled; } + + EventResult::Ignored } fn as_any(&self) -> &dyn Any { @@ -456,12 +568,12 @@ mod tests { #[test] fn test_checkbox_toggle() { - let checkbox = Checkbox::new(); + let mut checkbox = Checkbox::new(); assert!(!checkbox.is_checked()); - + checkbox.toggle(); assert!(checkbox.is_checked()); - + checkbox.toggle(); assert!(!checkbox.is_checked()); } @@ -476,13 +588,13 @@ mod tests { #[test] fn test_radio_button_selection() { - let radio = RadioButton::new("group1", "value1"); + let mut radio = RadioButton::new("group1", "value1"); assert!(!radio.is_selected()); - + radio.select(); assert!(radio.is_selected()); - + radio.deselect(); assert!(!radio.is_selected()); } -} \ No newline at end of file +} diff --git a/crates/strato-widgets/src/container.rs b/crates/strato-widgets/src/container.rs index 5e336aa..027fa11 100644 --- a/crates/strato-widgets/src/container.rs +++ b/crates/strato-widgets/src/container.rs @@ -1,15 +1,15 @@ //! Container widget for layout and styling -use crate::widget::{Widget, WidgetId, generate_id}; +use crate::widget::{generate_id, Widget, WidgetId}; +use std::any::Any; use strato_core::{ event::{Event, EventResult}, layout::{Constraints, EdgeInsets, Layout, Size}, - types::{Color, Rect, BorderRadius, Shadow, Point}, state::Signal, + types::{BorderRadius, Color, Point, Rect, Shadow}, Transform, }; use strato_renderer::batch::RenderBatch; -use std::any::Any; /// Container widget for grouping and styling child widgets pub struct Container { @@ -73,7 +73,12 @@ impl Container { /// Set padding with individual values pub fn padding_values(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self { - self.style.padding = EdgeInsets { top, right, bottom, left }; + self.style.padding = EdgeInsets { + top, + right, + bottom, + left, + }; self } @@ -165,7 +170,7 @@ impl Widget for Container { fn layout(&mut self, constraints: Constraints) -> Size { let constraints = self.constraints.unwrap_or(constraints); - + // Apply margin to constraints let margin = self.style.margin; let inner_constraints = Constraints { @@ -174,7 +179,7 @@ impl Widget for Container { min_height: (constraints.min_height - margin.vertical()).max(0.0), max_height: (constraints.max_height - margin.vertical()).max(0.0), }; - + // Apply padding to child constraints let padding = self.style.padding; let child_constraints = Constraints { @@ -183,18 +188,18 @@ impl Widget for Container { min_height: (inner_constraints.min_height - padding.vertical()).max(0.0), max_height: (inner_constraints.max_height - padding.vertical()).max(0.0), }; - + // Calculate child size let child_size = if let Some(child) = &mut self.child { child.layout(child_constraints) } else { Size::zero() }; - + // Calculate container size let mut width = child_size.width + padding.horizontal(); let mut height = child_size.height + padding.vertical(); - + // Apply fixed dimensions if specified if let Some(fixed_width) = self.style.width { width = fixed_width; @@ -202,11 +207,11 @@ impl Widget for Container { if let Some(fixed_height) = self.style.height { height = fixed_height; } - + // Add margin width += margin.horizontal(); height += margin.vertical(); - + // Constrain to limits Size::new( width.clamp(constraints.min_width, constraints.max_width), @@ -215,12 +220,17 @@ impl Widget for Container { } fn render(&self, batch: &mut RenderBatch, layout: Layout) { - let bounds = Rect::new(layout.position.x, layout.position.y, layout.size.width, layout.size.height); + let bounds = Rect::new( + layout.position.x, + layout.position.y, + layout.size.width, + layout.size.height, + ); self.bounds.set(bounds); let margin = self.style.margin; let padding = self.style.padding; - + // Calculate content rect (excluding margin) let content_rect = Rect::new( layout.position.x + margin.left, @@ -228,39 +238,36 @@ impl Widget for Container { layout.size.width - margin.horizontal(), layout.size.height - margin.vertical(), ); - + // Draw shadow if present if let Some(shadow) = &self.style.shadow { let _shadow_rect = content_rect.expand(shadow.spread_radius); // TODO: Implement proper shadow rendering } - + // Draw background with state feedback let mut background_color = self.style.background_color; let state = self.state.get(); - + if state.pressed { background_color = background_color.darken(0.2); // Visual feedback for press } else if state.hovered { - background_color = background_color.lighten(0.1); // Visual feedback for hover + background_color = background_color.lighten(0.1); // Visual feedback for hover } if background_color.a > 0.0 { batch.add_rect(content_rect, background_color, Transform::identity()); } - + // Draw border if self.style.border_width > 0.0 { // TODO: Implement proper border rendering } - + // Render child if let Some(child) = &self.child { let child_layout = Layout::new( - glam::Vec2::new( - content_rect.x + padding.left, - content_rect.y + padding.top, - ), + glam::Vec2::new(content_rect.x + padding.left, content_rect.y + padding.top), Size::new( content_rect.width - padding.horizontal(), content_rect.height - padding.vertical(), @@ -279,51 +286,48 @@ impl Widget for Container { let point = Point::new(mouse_event.position.x, mouse_event.position.y); let is_hovered = bounds.contains(point); let mut state = self.state.get(); - + if is_hovered != state.hovered { state.hovered = is_hovered; self.state.set(state); if let Some(handler) = &self.on_hover { handler(is_hovered); } - } if is_hovered { - // Don't necessarily block children, but track state + // Don't necessarily block children, but track state } } Event::MouseDown(mouse_event) => { - let bounds = self.bounds.get(); - let point = Point::new(mouse_event.position.x, mouse_event.position.y); - if bounds.contains(point) { - let mut state = self.state.get(); - state.pressed = true; - self.state.set(state); - - - } + let bounds = self.bounds.get(); + let point = Point::new(mouse_event.position.x, mouse_event.position.y); + if bounds.contains(point) { + let mut state = self.state.get(); + state.pressed = true; + self.state.set(state); + } } Event::MouseUp(mouse_event) => { let bounds = self.bounds.get(); let point = Point::new(mouse_event.position.x, mouse_event.position.y); let mut state = self.state.get(); - + if state.pressed { state.pressed = false; self.state.set(state); if bounds.contains(point) { - if let Some(handler) = &self.on_click { - handler(); - // If we clicked, we probably handled it. But child might have handled it? - // If child handled it, its result would be Handled. - } + if let Some(handler) = &self.on_click { + handler(); + // If we clicked, we probably handled it. But child might have handled it? + // If child handled it, its result would be Handled. + } } } } - _ => {} + _ => {} } } - + // Delegate to child FIRST to allow inner interactive elements to work if let Some(child) = &mut self.child { let child_result = child.handle_event(event); @@ -334,23 +338,24 @@ impl Widget for Container { // If child didn't handle it, AND we have interactions, check if we should handle it if self.on_click.is_some() { - match event { - Event::MouseDown(e) => { - let bounds = self.bounds.get(); - if bounds.contains(Point::new(e.position.x, e.position.y)) { - return EventResult::Handled; - } - } - Event::MouseUp(e) => { - let bounds = self.bounds.get(); - if bounds.contains(Point::new(e.position.x, e.position.y)) { // And was pressed logic... - return EventResult::Handled; - } - } - _ => {} - } + match event { + Event::MouseDown(e) => { + let bounds = self.bounds.get(); + if bounds.contains(Point::new(e.position.x, e.position.y)) { + return EventResult::Handled; + } + } + Event::MouseUp(e) => { + let bounds = self.bounds.get(); + if bounds.contains(Point::new(e.position.x, e.position.y)) { + // And was pressed logic... + return EventResult::Handled; + } + } + _ => {} + } } - + EventResult::Ignored } diff --git a/crates/strato-widgets/src/control.rs b/crates/strato-widgets/src/control.rs new file mode 100644 index 0000000..2347eb0 --- /dev/null +++ b/crates/strato-widgets/src/control.rs @@ -0,0 +1,254 @@ +//! Common control interaction and accessibility primitives. +//! +//! This module provides a lightweight state machine that widgets can embed to +//! unify pressed/hover/disabled handling and animate visual responses. It also +//! carries accessibility semantics (role, label, hint) so controls can expose +//! intent to higher level tooling. + +use crate::animation::Tween; +use crate::widget::WidgetState; +use strato_core::event::{Event, EventResult, KeyCode, MouseButton}; +use strato_core::state::Signal; +use strato_core::types::{Point, Rect}; + +/// The ARIA-like role associated with a control. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ControlRole { + Button, + Checkbox, + Radio, + Slider, + Input, + Toggle, +} + +impl Default for ControlRole { + fn default() -> Self { + ControlRole::Button + } +} + +/// Accessibility semantics for a control. +#[derive(Debug, Clone, Default)] +pub struct ControlSemantics { + pub role: ControlRole, + pub label: Option, + pub hint: Option, + pub value: Option, + pub toggled: Option, +} + +impl ControlSemantics { + pub fn new(role: ControlRole) -> Self { + Self { + role, + ..Default::default() + } + } +} + +/// Shared interaction state for focusable/pressable controls. +#[derive(Debug, Clone)] +pub struct ControlState { + state: Signal, + interaction_progress: Signal, + focus_progress: Signal, + semantics: ControlSemantics, +} + +impl ControlState { + /// Create a new control state for the given role. + pub fn new(role: ControlRole) -> Self { + Self { + state: Signal::new(WidgetState::Normal), + interaction_progress: Signal::new(0.0), + focus_progress: Signal::new(0.0), + semantics: ControlSemantics::new(role), + } + } + + /// Current widget state. + pub fn state(&self) -> WidgetState { + self.state.get() + } + + /// Set the widget state directly (used by tests/demo code). + pub fn set_state(&self, state: WidgetState) { + self.state.set(state); + } + + /// Mark the control as disabled/enabled. + pub fn set_disabled(&self, disabled: bool) { + if disabled { + self.state.set(WidgetState::Disabled); + } else if self.state.get() == WidgetState::Disabled { + self.state.set(WidgetState::Normal); + } + } + + /// Update the semantics label. + pub fn set_label(&mut self, label: impl Into) { + self.semantics.label = Some(label.into()); + } + + /// Update the semantics hint/description. + pub fn set_hint(&mut self, hint: impl Into) { + self.semantics.hint = Some(hint.into()); + } + + /// Update the semantics value (e.g., slider percentage). + pub fn set_value(&mut self, value: impl Into) { + self.semantics.value = Some(value.into()); + } + + /// Mark whether the control is toggled/checked. + pub fn set_toggled(&mut self, toggled: bool) { + self.semantics.toggled = Some(toggled); + } + + /// Access semantics for inspectors or higher-level layers. + pub fn semantics(&self) -> &ControlSemantics { + &self.semantics + } + + /// Smooth interaction animation state toward the target. + pub fn update(&self, delta_time: f32) { + let target_interaction = match self.state.get() { + WidgetState::Pressed => 1.0, + WidgetState::Hovered => 0.65, + WidgetState::Focused => 0.4, + WidgetState::Disabled => 0.0, + WidgetState::Normal => 0.0, + }; + let target_focus = if matches!(self.state.get(), WidgetState::Focused) { + 1.0 + } else { + 0.0 + }; + + let smooth = |current: f32, target: f32| { + let step = (target - current) * (delta_time * 8.0).clamp(0.0, 1.0); + (current + step).clamp(0.0, 1.0) + }; + + self.interaction_progress + .set(smooth(self.interaction_progress.get(), target_interaction)); + self.focus_progress + .set(smooth(self.focus_progress.get(), target_focus)); + } + + /// Overall interaction factor used for color/opacity blending. + pub fn interaction_factor(&self) -> f32 { + Tween::new(0.0, 1.0).transform( + self.interaction_progress + .get() + .max(self.focus_progress.get()), + ) + } + + /// Update hover state when the cursor enters/leaves the control bounds. + pub fn hover(&self, within: bool) { + if self.state.get() == WidgetState::Disabled { + return; + } + match (within, self.state.get()) { + (true, WidgetState::Normal) => self.state.set(WidgetState::Hovered), + (false, WidgetState::Hovered) => self.state.set(WidgetState::Normal), + (false, WidgetState::Pressed) => self.state.set(WidgetState::Normal), + _ => {} + } + } + + /// Start a press interaction if the point is inside. + pub fn press(&self, point: Point, bounds: Rect) -> bool { + if self.state.get() == WidgetState::Disabled { + return false; + } + if bounds.contains(point) { + self.state.set(WidgetState::Pressed); + return true; + } + false + } + + /// Finish a press interaction and report whether activation should occur. + pub fn release(&self, point: Point, bounds: Rect) -> bool { + if self.state.get() == WidgetState::Disabled { + return false; + } + if self.state.get() == WidgetState::Pressed { + let is_hovered = bounds.contains(point); + self.state.set(if is_hovered { + WidgetState::Hovered + } else { + WidgetState::Normal + }); + return is_hovered; + } + false + } + + /// Update focus state based on keyboard navigation. + pub fn focus(&self) { + if self.state.get() != WidgetState::Disabled { + self.state.set(WidgetState::Focused); + } + } + + /// Handle blur. + pub fn blur(&self) { + if self.state.get() == WidgetState::Focused { + self.state.set(WidgetState::Normal); + } + } + + /// Keyboard activations for accessible triggers. + pub fn handle_keyboard_activation(&self, event: &Event) -> EventResult { + match event { + Event::KeyDown(key) if matches!(key.key_code, KeyCode::Enter | KeyCode::Space) => { + if self.state.get() != WidgetState::Disabled { + self.state.set(WidgetState::Pressed); + return EventResult::Handled; + } + } + Event::KeyUp(key) if matches!(key.key_code, KeyCode::Enter | KeyCode::Space) => { + if self.state.get() == WidgetState::Pressed { + self.state.set(WidgetState::Focused); + return EventResult::Handled; + } + } + _ => {} + } + EventResult::Ignored + } + + /// Pointer-based interaction dispatcher. + pub fn handle_pointer_event(&self, event: &Event, bounds: Rect) -> EventResult { + match event { + Event::MouseMove(mouse) => { + let point = Point::new(mouse.position.x, mouse.position.y); + self.hover(bounds.contains(point)); + EventResult::Ignored + } + Event::MouseDown(mouse) => { + if let Some(MouseButton::Left) = mouse.button { + let point = Point::new(mouse.position.x, mouse.position.y); + if self.press(point, bounds) { + return EventResult::Handled; + } + } + EventResult::Ignored + } + Event::MouseUp(mouse) => { + if let Some(MouseButton::Left) = mouse.button { + let point = Point::new(mouse.position.x, mouse.position.y); + if self.release(point, bounds) { + return EventResult::Handled; + } + } + EventResult::Ignored + } + _ => EventResult::Ignored, + } + } +} diff --git a/crates/strato-widgets/src/dropdown.rs b/crates/strato-widgets/src/dropdown.rs index 9e48d45..363bed1 100644 --- a/crates/strato-widgets/src/dropdown.rs +++ b/crates/strato-widgets/src/dropdown.rs @@ -1,17 +1,15 @@ //! Dropdown and Select widgets implementation for StratoUI -use crate::widget::{Widget, WidgetId, generate_id}; +use crate::widget::{generate_id, Widget, WidgetId}; use strato_core::{ - event::{Event, EventResult, KeyCode, KeyboardEvent, MouseEvent, MouseButton}, - layout::{Size, Constraints, Layout}, + event::{Event, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseEvent}, + layout::{Constraints, Layout, Size}, state::Signal, - types::{Rect, Color}, types::Transform, + types::{Color, Rect}, vdom::VNode, }; -use strato_renderer::{ - batch::RenderBatch, -}; +use strato_renderer::batch::RenderBatch; /// Dropdown/Select widget for choosing from a list of options #[derive(Debug, Clone)] @@ -62,17 +60,17 @@ impl Default for DropdownStyle { fn default() -> Self { Self { background_color: [1.0, 1.0, 1.0, 1.0], // White - border_color: [0.8, 0.8, 0.8, 1.0], // Light gray + border_color: [0.8, 0.8, 0.8, 1.0], // Light gray border_width: 1.0, border_radius: 4.0, - text_color: [0.2, 0.2, 0.2, 1.0], // Dark gray - placeholder_color: [0.6, 0.6, 0.6, 1.0], // Medium gray - hover_color: [0.95, 0.95, 0.95, 1.0], // Light gray - selected_color: [0.2, 0.6, 1.0, 1.0], // Blue - disabled_color: [0.9, 0.9, 0.9, 1.0], // Light gray - dropdown_background: [1.0, 1.0, 1.0, 1.0], // White + text_color: [0.2, 0.2, 0.2, 1.0], // Dark gray + placeholder_color: [0.6, 0.6, 0.6, 1.0], // Medium gray + hover_color: [0.95, 0.95, 0.95, 1.0], // Light gray + selected_color: [0.2, 0.6, 1.0, 1.0], // Blue + disabled_color: [0.9, 0.9, 0.9, 1.0], // Light gray + dropdown_background: [1.0, 1.0, 1.0, 1.0], // White dropdown_border_color: [0.7, 0.7, 0.7, 1.0], // Gray - dropdown_shadow: [0.0, 0.0, 0.0, 0.1], // Light shadow + dropdown_shadow: [0.0, 0.0, 0.0, 0.1], // Light shadow font_size: 14.0, padding: 8.0, } @@ -206,7 +204,8 @@ impl Dropdown { /// Get the selected value pub fn get_selected(&self) -> Option<&T> { - self.selected_index.get() + self.selected_index + .get() .and_then(|index| self.options.get(index)) .map(|opt| &opt.value) } @@ -259,7 +258,7 @@ impl Dropdown { /// Get filtered options based on search fn filtered_options(&self) -> Vec<(usize, &DropdownOption)> { let search = self.search_text.get().to_lowercase(); - + if search.is_empty() { self.options.iter().enumerate().collect() } else { @@ -283,7 +282,7 @@ impl Dropdown { let dropdown_y = bounds.y + self.height; let option_height = self.height; let filtered_options = self.filtered_options(); - + if event.position.y >= dropdown_y { let option_index = ((event.position.y - dropdown_y) / option_height) as usize; if let Some((original_index, _)) = filtered_options.get(option_index) { @@ -291,7 +290,7 @@ impl Dropdown { return EventResult::Handled; } } - + // Click outside dropdown - close it self.close(); } else { @@ -331,16 +330,17 @@ impl Dropdown { if self.is_open() { let filtered = self.filtered_options(); let current = self.selected_index.get(); - + let next_index = if let Some(current_idx) = current { - filtered.iter() + filtered + .iter() .position(|(idx, _)| *idx == current_idx) .map(|pos| (pos + 1).min(filtered.len() - 1)) .unwrap_or(0) } else { 0 }; - + if let Some((original_idx, _)) = filtered.get(next_index) { self.selected_index.set(Some(*original_idx)); } @@ -353,16 +353,17 @@ impl Dropdown { if self.is_open() { let filtered = self.filtered_options(); let current = self.selected_index.get(); - + let prev_index = if let Some(current_idx) = current { - filtered.iter() + filtered + .iter() .position(|(idx, _)| *idx == current_idx) .map(|pos| pos.saturating_sub(1)) .unwrap_or(0) } else { filtered.len().saturating_sub(1) }; - + if let Some((original_idx, _)) = filtered.get(prev_index) { self.selected_index.set(Some(*original_idx)); } @@ -396,8 +397,6 @@ impl Dropdown { } } } - - } impl Default for Dropdown { @@ -406,7 +405,9 @@ impl Default for Dro } } -impl Widget for Dropdown { +impl Widget + for Dropdown +{ fn id(&self) -> WidgetId { self.id } @@ -417,7 +418,12 @@ impl { // Check if click is outside - let point = strato_core::types::Point::new(mouse_event.position.x, mouse_event.position.y); - + let point = + strato_core::types::Point::new(mouse_event.position.x, mouse_event.position.y); + // If open, check if we clicked inside the list if self.is_open.get() { - let list_height = (self.filtered_options().len() as f32 * self.height).min(self.max_height); + let list_height = + (self.filtered_options().len() as f32 * self.height).min(self.max_height); let list_bounds = Rect::new( bounds.x, bounds.y + bounds.height, bounds.width, - list_height + list_height, ); - + if list_bounds.contains(point) { return self.handle_mouse_event(mouse_event, bounds); } @@ -569,12 +594,12 @@ impl { self.handle_keyboard_event(keyboard_event) - }, + } _ => EventResult::Ignored, } } @@ -609,7 +634,7 @@ mod tests { .add_value("Option 1".to_string()) .add_value("Option 2".to_string()) .add_option("Option 3".to_string(), "Custom Label".to_string()); - + assert_eq!(dropdown.options.len(), 3); assert_eq!(dropdown.options[2].label, "Custom Label"); } @@ -620,7 +645,7 @@ mod tests { .add_value("Option 1".to_string()) .add_value("Option 2".to_string()) .selected("Option 2".to_string()); - + assert_eq!(dropdown.get_selected_index(), Some(1)); assert_eq!(dropdown.get_selected(), Some(&"Option 2".to_string())); } @@ -628,11 +653,11 @@ mod tests { #[test] fn test_dropdown_toggle() { let dropdown: Dropdown = Dropdown::new(); - + assert!(!dropdown.is_open()); dropdown.open(); assert!(dropdown.is_open()); dropdown.close(); assert!(!dropdown.is_open()); } -} \ No newline at end of file +} diff --git a/crates/strato-widgets/src/grid.rs b/crates/strato-widgets/src/grid.rs index 6691815..93bbcaa 100644 --- a/crates/strato-widgets/src/grid.rs +++ b/crates/strato-widgets/src/grid.rs @@ -1,11 +1,11 @@ //! Grid widget for 2D layout -use crate::widget::{Widget, WidgetId, generate_id}; +use crate::widget::{generate_id, Widget, WidgetId}; +use std::any::Any; use strato_core::{ event::{Event, EventResult}, layout::{Constraints, Layout, Size}, }; use strato_renderer::batch::RenderBatch; -use std::any::Any; /// Unit for grid tracks (rows/columns) #[derive(Debug, Clone, Copy, PartialEq)] @@ -90,14 +90,14 @@ impl Widget for Grid { fn layout(&mut self, constraints: Constraints) -> Size { // If no columns defined, default to 1 column auto if self.cols.is_empty() { - self.cols.push(GridUnit::Auto); + self.cols.push(GridUnit::Auto); } // If no rows defined, we will implicitly add auto rows as needed - + let num_cols = self.cols.len(); let num_children = self.children.len(); let implicit_rows_needed = (num_children as f32 / num_cols as f32).ceil() as usize; - + // Final rows list including implicit ones let mut final_rows = self.rows.clone(); while final_rows.len() < implicit_rows_needed { @@ -113,7 +113,7 @@ impl Widget for Grid { // First pass: Calculate fixed and auto sizes let mut col_widths = vec![0.0; num_cols]; let mut row_heights = vec![0.0; num_rows]; - + // Helper to get child at (row, col) let get_child_idx = |r, c| r * num_cols + c; @@ -121,7 +121,9 @@ impl Widget for Grid { for r in 0..num_rows { for c in 0..num_cols { let idx = get_child_idx(r, c); - if idx >= self.children.len() { continue; } + if idx >= self.children.len() { + continue; + } let is_col_auto = matches!(self.cols[c], GridUnit::Auto); let is_row_auto = matches!(final_rows[r], GridUnit::Auto); @@ -132,7 +134,7 @@ impl Widget for Grid { // We measure with loose constraints to get content size. let measure_constraints = Constraints::loose(available_width, available_height); let size = self.children[idx].layout(measure_constraints); - + if is_col_auto { col_widths[c] = f32::max(col_widths[c], size.width); } @@ -150,16 +152,23 @@ impl Widget for Grid { } } for (r, unit) in final_rows.iter().enumerate() { - if let GridUnit::Pixel(px) = unit { + if let GridUnit::Pixel(px) = unit { row_heights[r] = *px; } } // Measure FRACTION tracks - let used_width: f32 = col_widths.iter().sum::() + (num_cols.saturating_sub(1) as f32 * self.col_gap); + let used_width: f32 = + col_widths.iter().sum::() + (num_cols.saturating_sub(1) as f32 * self.col_gap); let remaining_width = (available_width - used_width).max(0.0); - let total_col_fr: f32 = self.cols.iter().fold(0.0, |acc, u| if let GridUnit::Fraction(fr) = u { acc + fr } else { acc }); - + let total_col_fr: f32 = self.cols.iter().fold(0.0, |acc, u| { + if let GridUnit::Fraction(fr) = u { + acc + fr + } else { + acc + } + }); + if total_col_fr > 0.0 { for (c, unit) in self.cols.iter().enumerate() { if let GridUnit::Fraction(fr) = unit { @@ -172,19 +181,26 @@ impl Widget for Grid { // If we have infinite height constraint, fractions might resolve to 0 or behave like Auto. // Here we assume if height is constrained, we distribute. if available_height.is_finite() { - let used_height: f32 = row_heights.iter().sum::() + (num_rows.saturating_sub(1) as f32 * self.row_gap); - let remaining_height = (available_height - used_height).max(0.0); - let total_row_fr: f32 = final_rows.iter().fold(0.0, |acc, u| if let GridUnit::Fraction(fr) = u { acc + fr } else { acc }); + let used_height: f32 = row_heights.iter().sum::() + + (num_rows.saturating_sub(1) as f32 * self.row_gap); + let remaining_height = (available_height - used_height).max(0.0); + let total_row_fr: f32 = final_rows.iter().fold(0.0, |acc, u| { + if let GridUnit::Fraction(fr) = u { + acc + fr + } else { + acc + } + }); - if total_row_fr > 0.0 { + if total_row_fr > 0.0 { for (r, unit) in final_rows.iter().enumerate() { if let GridUnit::Fraction(fr) = unit { row_heights[r] = (fr / total_row_fr) * remaining_height; } } - } - } - // If height is infinite, treat fractions as auto or 0? + } + } + // If height is infinite, treat fractions as auto or 0? // For now, let's treat as 0 or maybe min size. In real CSS grid they collapse to content if height is indefinite. // We leave them as 0 if not calculated above, unless we implement content-based minimums for fr tracks. @@ -197,34 +213,34 @@ impl Widget for Grid { for r in 0..num_rows { let mut current_x = 0.0; let row_h = row_heights[r]; - + for c in 0..num_cols { let idx = get_child_idx(r, c); let col_w = col_widths[c]; - + if idx < self.children.len() { let cell_x = current_x; let cell_y = current_y; - + // Re-layout child with exact cell size // We force the child to fit the cell? Or align it? // Typically grid items stretch to fill cell unless aligned. // We'll enforce loose constraints up to cell size, but tight might be better for stretch. // Let's use tight for compatibility with "stretch" default behavior. - let cell_constraints = Constraints::tight(col_w, row_h); + let cell_constraints = Constraints::tight(col_w, row_h); // Note: If row_h is 0 (e.g. empty fr track), this hides the child. - + self.children[idx].layout(cell_constraints); self.cached_child_layouts.push(Layout::new( glam::Vec2::new(cell_x, cell_y), - Size::new(col_w, row_h) + Size::new(col_w, row_h), )); } current_x += col_w + self.col_gap; } - + total_width = total_width.max(current_x - self.col_gap); // remove last gap current_y += row_h + self.row_gap; } @@ -236,10 +252,8 @@ impl Widget for Grid { fn render(&self, batch: &mut RenderBatch, layout: Layout) { for (i, child) in self.children.iter().enumerate() { if let Some(child_layout) = self.cached_child_layouts.get(i) { - let absolute_layout = Layout::new( - layout.position + child_layout.position, - child_layout.size, - ); + let absolute_layout = + Layout::new(layout.position + child_layout.position, child_layout.size); child.render(batch, absolute_layout); } } @@ -274,7 +288,7 @@ impl Widget for Grid { } fn clone_widget(&self) -> Box { - Box::new(Grid { + Box::new(Grid { id: generate_id(), children: self.children.iter().map(|c| c.clone_widget()).collect(), rows: self.rows.clone(), diff --git a/crates/strato-widgets/src/image.rs b/crates/strato-widgets/src/image.rs index 5a51414..735d493 100644 --- a/crates/strato-widgets/src/image.rs +++ b/crates/strato-widgets/src/image.rs @@ -2,17 +2,17 @@ //! //! Supports various image formats, scaling modes, and loading states. +use crate::widget::{generate_id, Widget, WidgetContext, WidgetId}; +use std::path::PathBuf; +use std::sync::Arc; use strato_core::{ event::{Event, EventResult}, - layout::{Constraints, Size, Layout}, - types::{Rect, Color, Transform, Point}, + layout::{Constraints, Layout, Size}, state::Signal, + types::{Color, Point, Rect, Transform}, vdom::VNode, }; use strato_renderer::batch::RenderBatch; -use crate::widget::{Widget, WidgetId, WidgetContext, generate_id}; -use std::path::PathBuf; -use std::sync::Arc; /// Image scaling modes #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -67,7 +67,11 @@ pub enum ImageSource { /// Use embedded data Data(ImageData), /// Use placeholder - Placeholder { width: u32, height: u32, color: Color }, + Placeholder { + width: u32, + height: u32, + color: Color, + }, } /// Image widget styling @@ -198,7 +202,11 @@ impl Image { /// Create placeholder image pub fn placeholder(width: u32, height: u32, color: Color) -> Self { - Self::new(ImageSource::Placeholder { width, height, color }) + Self::new(ImageSource::Placeholder { + width, + height, + color, + }) } /// Set image fit mode @@ -292,26 +300,24 @@ impl Image { pub fn load_image(&self) { let source = self.source.clone(); let state = self.state.clone(); - + // Mark as loading state.set(ImageState::Loading); match source { ImageSource::File(path) => { let state = state.clone(); - std::thread::spawn(move || { - match std::fs::read(&path) { - Ok(bytes) => { - if let Ok(data) = decode_image_data_internal(bytes) { - state.set(ImageState::Loaded(data)); - } else { - state.set(ImageState::Error("Failed to decode image".to_string())); - } - } - Err(e) => { - state.set(ImageState::Error(format!("Failed to load image: {}", e))); + std::thread::spawn(move || match std::fs::read(&path) { + Ok(bytes) => { + if let Ok(data) = decode_image_data_internal(bytes) { + state.set(ImageState::Loaded(data)); + } else { + state.set(ImageState::Error("Failed to decode image".to_string())); } } + Err(e) => { + state.set(ImageState::Error(format!("Failed to load image: {}", e))); + } }); } ImageSource::Url(url) => { @@ -319,25 +325,36 @@ impl Image { std::thread::spawn(move || { // Fetch image data let client = reqwest::blocking::Client::new(); - match client.get(&url) + match client + .get(&url) .header("User-Agent", "StratoUI/0.1.0") - .send() { + .send() + { Ok(response) => { if response.status().is_success() { match response.bytes() { Ok(bytes) => { - if let Ok(data) = decode_image_data_internal(bytes.to_vec()) { + if let Ok(data) = decode_image_data_internal(bytes.to_vec()) + { state.set(ImageState::Loaded(data)); } else { - state.set(ImageState::Error("Failed to decode image from URL".to_string())); + state.set(ImageState::Error( + "Failed to decode image from URL".to_string(), + )); } } Err(e) => { - state.set(ImageState::Error(format!("Failed to read bytes: {}", e))); + state.set(ImageState::Error(format!( + "Failed to read bytes: {}", + e + ))); } } } else { - state.set(ImageState::Error(format!("HTTP Error: {}", response.status()))); + state.set(ImageState::Error(format!( + "HTTP Error: {}", + response.status() + ))); } } Err(e) => { @@ -349,7 +366,11 @@ impl Image { ImageSource::Data(data) => { state.set(ImageState::Loaded(data)); } - ImageSource::Placeholder { width, height, color } => { + ImageSource::Placeholder { + width, + height, + color, + } => { let data = create_placeholder_data_internal(width, height, color); state.set(ImageState::Loaded(data)); } @@ -364,7 +385,7 @@ fn decode_image_data_internal(bytes: Vec) -> Result { let rgba_image = dynamic_image.to_rgba8(); let (width, height) = rgba_image.dimensions(); let data = rgba_image.into_raw(); - + Ok(ImageData { width, height, @@ -379,16 +400,16 @@ fn decode_image_data_internal(bytes: Vec) -> Result { fn create_placeholder_data_internal(width: u32, height: u32, color: Color) -> ImageData { let pixel_count = (width * height) as usize; let mut data = Vec::with_capacity(pixel_count * 4); - + let r = (color.r * 255.0) as u8; let g = (color.g * 255.0) as u8; let b = (color.b * 255.0) as u8; let a = (color.a * 255.0) as u8; - + for _ in 0..pixel_count { data.extend_from_slice(&[r, g, b, a]); } - + ImageData { width, height, @@ -397,14 +418,15 @@ fn create_placeholder_data_internal(width: u32, height: u32, color: Color) -> Im } } -impl Image { // Re-opening impl to fix the struct definition gap if needed, but here we are replacing methods. - +impl Image { + // Re-opening impl to fix the struct definition gap if needed, but here we are replacing methods. fn calculate_display_size(&self, container_size: Size, image_size: Size) -> (Size, Rect) { match self.style.fit { - ImageFit::Fill => { - (container_size, Rect::new(0.0, 0.0, container_size.width, container_size.height)) - } + ImageFit::Fill => ( + container_size, + Rect::new(0.0, 0.0, container_size.width, container_size.height), + ), ImageFit::Contain => { let scale = (container_size.width / image_size.width) .min(container_size.height / image_size.height); @@ -412,7 +434,10 @@ impl Image { // Re-opening impl to fix the struct definition gap if needed, but let scaled_height = image_size.height * scale; let x = (container_size.width - scaled_width) / 2.0; let y = (container_size.height - scaled_height) / 2.0; - (Size::new(scaled_width, scaled_height), Rect::new(x, y, scaled_width, scaled_height)) + ( + Size::new(scaled_width, scaled_height), + Rect::new(x, y, scaled_width, scaled_height), + ) } ImageFit::Cover => { let scale = (container_size.width / image_size.width) @@ -421,13 +446,21 @@ impl Image { // Re-opening impl to fix the struct definition gap if needed, but let scaled_height = image_size.height * scale; let x = (container_size.width - scaled_width) / 2.0; let y = (container_size.height - scaled_height) / 2.0; - (Size::new(scaled_width, scaled_height), Rect::new(x, y, scaled_width, scaled_height)) + ( + Size::new(scaled_width, scaled_height), + Rect::new(x, y, scaled_width, scaled_height), + ) } ImageFit::ScaleDown => { - if image_size.width <= container_size.width && image_size.height <= container_size.height { + if image_size.width <= container_size.width + && image_size.height <= container_size.height + { let x = (container_size.width - image_size.width) / 2.0; let y = (container_size.height - image_size.height) / 2.0; - (image_size, Rect::new(x, y, image_size.width, image_size.height)) + ( + image_size, + Rect::new(x, y, image_size.width, image_size.height), + ) } else { self.calculate_display_size(container_size, image_size) // Use contain logic } @@ -435,7 +468,10 @@ impl Image { // Re-opening impl to fix the struct definition gap if needed, but ImageFit::None => { let x = (container_size.width - image_size.width) / 2.0; let y = (container_size.height - image_size.height) / 2.0; - (image_size, Rect::new(x, y, image_size.width, image_size.height)) + ( + image_size, + Rect::new(x, y, image_size.width, image_size.height), + ) } } } @@ -500,12 +536,10 @@ impl Widget for Image { ); self.bounds.set(bounds); - let mut background_color = self.style.background_color.unwrap_or(Color::rgba( - 0.0, - 0.0, - 0.0, - 0.0, - )); + let mut background_color = self + .style + .background_color + .unwrap_or(Color::rgba(0.0, 0.0, 0.0, 0.0)); match self.state.get() { ImageState::Loaded(data) => { @@ -524,12 +558,17 @@ impl Widget for Image { // TODO: Implement proper rounded textured quad in renderer // For now, we render the image as a standard textured quad // and apply border radius to the container background if set - + // Render background if set if background_color.a > 0.0 { - batch.add_rounded_rect(bounds, background_color, self.style.border_radius, Transform::identity()); + batch.add_rounded_rect( + bounds, + background_color, + self.style.border_radius, + Transform::identity(), + ); } - + // Render Image batch.add_image( self.id, @@ -537,13 +576,12 @@ impl Widget for Image { data.width, data.height, image_rect, - Color::rgba(1.0, 1.0, 1.0, self.style.opacity) + Color::rgba(1.0, 1.0, 1.0, self.style.opacity), ); - } else { // Render background if set if background_color.a > 0.0 { - batch.add_rect(bounds, background_color, Transform::identity()); + batch.add_rect(bounds, background_color, Transform::identity()); } // Render Image @@ -553,7 +591,7 @@ impl Widget for Image { data.width, data.height, image_rect, - Color::rgba(1.0, 1.0, 1.0, self.style.opacity) + Color::rgba(1.0, 1.0, 1.0, self.style.opacity), ); } } @@ -579,7 +617,7 @@ impl Widget for Image { style: self.style.clone(), state: self.state.clone(), alt_text: self.alt_text.clone(), - on_load: None, // Cannot clone function pointers + on_load: None, // Cannot clone function pointers on_error: None, // Cannot clone function pointers on_click: None, // Cannot clone function pointers loading_placeholder: self.loading_placeholder.clone(), @@ -698,12 +736,14 @@ mod tests { #[test] fn test_image_builder() { - let image = ImageBuilder::new(ImageSource::Url("https://example.com/image.png".to_string())) - .fit(ImageFit::Cover) - .opacity(0.8) - .border_radius(10.0) - .alt_text("Test image") - .build(); + let image = ImageBuilder::new(ImageSource::Url( + "https://example.com/image.png".to_string(), + )) + .fit(ImageFit::Cover) + .opacity(0.8) + .border_radius(10.0) + .alt_text("Test image") + .build(); assert_eq!(image.style.fit, ImageFit::Cover); assert_eq!(image.style.opacity, 0.8); @@ -715,8 +755,13 @@ mod tests { fn test_placeholder_image() { let color = Color::rgba(1.0, 0.0, 0.0, 1.0); // Red let image = Image::placeholder(100, 100, color); - - if let ImageSource::Placeholder { width, height, color: c } = image.source { + + if let ImageSource::Placeholder { + width, + height, + color: c, + } = image.source + { assert_eq!(width, 100); assert_eq!(height, 100); assert_eq!(c, color); @@ -732,7 +777,7 @@ mod tests { let image_size = Size::new(100.0, 100.0); let (display_size, rect) = image.calculate_display_size(container_size, image_size); - + // For contain fit, should maintain aspect ratio assert!(display_size.width <= container_size.width); assert!(display_size.height <= container_size.height); @@ -740,9 +785,8 @@ mod tests { #[test] fn test_image_filters() { - let image = Image::from_file("test.png") - .filter(ImageFilter::Blur(5.0)); - + let image = Image::from_file("test.png").filter(ImageFilter::Blur(5.0)); + assert!(matches!(image.style.filter, ImageFilter::Blur(5.0))); } } diff --git a/crates/strato-widgets/src/input.rs b/crates/strato-widgets/src/input.rs index c50931c..0b64419 100644 --- a/crates/strato-widgets/src/input.rs +++ b/crates/strato-widgets/src/input.rs @@ -1,21 +1,21 @@ //! Text input widget implementation -//! +//! //! Provides text input components with various input types, validation, and formatting options. +use crate::widget::{generate_id, Widget, WidgetId}; +use std::{any::Any, sync::Arc}; use strato_core::{ - layout::{Size, Constraints, Layout}, - state::{Signal}, - theme::{Theme}, - types::{Point, Rect, Color, Transform}, - event::{Event, EventResult, KeyboardEvent, KeyCode, KeyEvent, MouseEvent}, - vdom::{VNode}, + event::{Event, EventResult, KeyCode, KeyEvent, KeyboardEvent, MouseEvent}, + layout::{Constraints, Layout, Size}, + state::Signal, + theme::Theme, + types::{Color, Point, Rect, Transform}, + vdom::VNode, }; use strato_renderer::{ - vertex::{Vertex, VertexBuilder}, batch::RenderBatch, + vertex::{Vertex, VertexBuilder}, }; -use crate::widget::{Widget, WidgetId, generate_id}; -use std::{sync::Arc, any::Any}; /// Input type enumeration #[derive(Debug, Clone, Copy, PartialEq)] @@ -72,16 +72,16 @@ impl Default for InputStyle { // Use platform-specific default fonts #[cfg(target_os = "windows")] let font_family = "Segoe UI"; - + #[cfg(target_os = "macos")] let font_family = "SF Pro Display"; - + #[cfg(target_os = "linux")] let font_family = "Ubuntu"; - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let font_family = "Arial"; - + Self { background_color: Color::WHITE, border_color: Color::GRAY, @@ -164,37 +164,37 @@ pub struct TextInput { multiline: bool, rows: usize, cols: usize, - + // State management state: Signal, validation_state: Signal, validation_message: Signal>, focused: Signal, hovered: Signal, - + // Cursor and selection cursor_position: Signal, selection_start: Signal>, selection_end: Signal>, - + // Layout and rendering bounds: Signal, content_bounds: Signal, visible: Signal, - + // Styling style: InputStyle, theme: Option>, - + // Validation validators: Vec, - + // Event handlers on_change: Option>, on_focus: Option>, on_blur: Option>, on_submit: Option>, - + // Internal state cursor_blink_timer: Signal, scroll_offset: Signal, @@ -229,11 +229,23 @@ impl std::fmt::Debug for TextInput { .field("visible", &self.visible) .field("style", &self.style) .field("theme", &self.theme) - .field("validators", &format!("{} validators", self.validators.len())) - .field("on_change", &self.on_change.as_ref().map(|_| "Some(callback)")) - .field("on_focus", &self.on_focus.as_ref().map(|_| "Some(callback)")) + .field( + "validators", + &format!("{} validators", self.validators.len()), + ) + .field( + "on_change", + &self.on_change.as_ref().map(|_| "Some(callback)"), + ) + .field( + "on_focus", + &self.on_focus.as_ref().map(|_| "Some(callback)"), + ) .field("on_blur", &self.on_blur.as_ref().map(|_| "Some(callback)")) - .field("on_submit", &self.on_submit.as_ref().map(|_| "Some(callback)")) + .field( + "on_submit", + &self.on_submit.as_ref().map(|_| "Some(callback)"), + ) .field("cursor_blink_timer", &self.cursor_blink_timer) .field("scroll_offset", &self.scroll_offset) .finish() @@ -257,37 +269,37 @@ impl TextInput { multiline: false, rows: 1, cols: 20, - + // State management state: Signal::new(InputState::Normal), validation_state: Signal::new(ValidationState::Valid), validation_message: Signal::new(None), focused: Signal::new(false), hovered: Signal::new(false), - + // Cursor and selection cursor_position: Signal::new(0), selection_start: Signal::new(None), selection_end: Signal::new(None), - + // Layout and rendering bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), content_bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), visible: Signal::new(true), - + // Styling style: InputStyle::default(), theme: None, - + // Validation validators: Vec::new(), - + // Event handlers on_change: None, on_focus: None, on_blur: None, on_submit: None, - + // Internal state cursor_blink_timer: Signal::new(0.0), scroll_offset: Signal::new(0.0), @@ -392,7 +404,7 @@ impl TextInput { } /// Add validator - pub fn validator(mut self, validator: F) -> Self + pub fn validator(mut self, validator: F) -> Self where F: Fn(&str) -> Result<(), String> + Send + Sync + 'static, { @@ -449,19 +461,19 @@ impl TextInput { /// Set value programmatically pub fn set_value(&self, value: impl Into) { let new_value = value.into(); - + // Validate length constraints if let Some(max_len) = self.max_length { if new_value.len() > max_len { return; } } - + self.value.set(new_value.clone()); - + // Trigger validation self.validate(); - + // Trigger change callback if let Some(ref callback) = self.on_change { callback(&new_value); @@ -509,7 +521,7 @@ impl TextInput { if !self.is_disabled() && !self.is_readonly() { self.focused.set(true); self.state.set(InputState::Focused); - + // Trigger focus callback if let Some(ref callback) = self.on_focus { callback(); @@ -521,7 +533,7 @@ impl TextInput { pub fn blur(&self) { self.focused.set(false); self.clear_selection(); - + // Update state if self.is_disabled() { self.state.set(InputState::Disabled); @@ -532,7 +544,7 @@ impl TextInput { } else { self.state.set(InputState::Normal); } - + // Trigger blur callback if let Some(ref callback) = self.on_blur { callback(); @@ -542,31 +554,34 @@ impl TextInput { /// Validate input pub fn validate(&self) -> bool { let value = self.value.get(); - + // Check required if self.required && value.is_empty() { self.validation_state.set(ValidationState::Invalid); - self.validation_message.set(Some("This field is required".to_string())); + self.validation_message + .set(Some("This field is required".to_string())); return false; } - + // Check length constraints if let Some(min_len) = self.min_length { if value.len() < min_len { self.validation_state.set(ValidationState::Invalid); - self.validation_message.set(Some(format!("Minimum length is {}", min_len))); + self.validation_message + .set(Some(format!("Minimum length is {}", min_len))); return false; } } - + if let Some(max_len) = self.max_length { if value.len() > max_len { self.validation_state.set(ValidationState::Invalid); - self.validation_message.set(Some(format!("Maximum length is {}", max_len))); + self.validation_message + .set(Some(format!("Maximum length is {}", max_len))); return false; } } - + // Run custom validators for validator in &self.validators { if let Err(error) = validator(&value) { @@ -575,7 +590,7 @@ impl TextInput { return false; } } - + self.validation_state.set(ValidationState::Valid); self.validation_message.set(None); true @@ -585,7 +600,7 @@ impl TextInput { pub fn calculate_size(&self, available_size: Size) -> Size { let style = self.style.for_state(self.state.get()); let padding = style.padding; - + let text_width = if self.multiline { if available_size.width.is_finite() { available_size.width - padding.1 - padding.3 @@ -595,13 +610,13 @@ impl TextInput { } else { (self.cols as f32) * (style.font_size * 0.6) // Approximate character width }; - + let text_height = if self.multiline { (self.rows as f32) * (style.font_size * style.line_height) } else { style.font_size * style.line_height }; - + Size::new( text_width + padding.1 + padding.3, text_height + padding.0 + padding.2, @@ -611,17 +626,17 @@ impl TextInput { /// Layout the input pub fn layout(&self, bounds: Rect) { self.bounds.set(bounds); - + let style = self.style.for_state(self.state.get()); let padding = style.padding; - + let content_bounds = Rect::new( bounds.x + padding.3, bounds.y + padding.0, bounds.width - padding.1 - padding.3, bounds.height - padding.0 - padding.2, ); - + self.content_bounds.set(content_bounds); } @@ -629,31 +644,31 @@ impl TextInput { pub fn handle_mouse_event(&self, event: &MouseEvent) -> bool { let bounds = self.bounds.get(); let point = Point::new(event.position.x, event.position.y); - + if !bounds.contains(point) { return false; } - + match event.button { Some(strato_core::event::MouseButton::Left) => { // For mouse down events, we need to check if this is a press event // Since MouseEvent doesn't have a pressed field, we'll assume this is called for press events self.focus(); - + // Calculate cursor position from click let content_bounds = self.content_bounds.get(); let relative_x = point.x - content_bounds.x; - + // Simple cursor positioning (would need proper text measurement) let char_width = self.style.font_size * 0.6; let cursor_pos = ((relative_x / char_width) as usize).min(self.value.get().len()); self.cursor_position.set(cursor_pos); - + return true; } _ => {} } - + false } @@ -662,7 +677,7 @@ impl TextInput { if !self.is_focused() || self.is_disabled() || self.is_readonly() { return false; } - + // Handle text input from KeyboardEvent // NOTE: In many systems, character input comes via Event::TextInput, not KeyboardEvent::text // We keep this for compatibility if the platform sends text here. @@ -675,7 +690,7 @@ impl TextInput { } return true; } - + // Handle special keys match event.key_code { KeyCode::Backspace => { @@ -718,25 +733,25 @@ impl TextInput { fn insert_char(&self, ch: char) { let mut value = self.value.get(); let cursor_pos = self.cursor_position.get(); - + // Check max length if let Some(max_len) = self.max_length { if value.len() >= max_len { return; } } - + // Insert character if cursor_pos <= value.len() { value.insert(cursor_pos, ch); self.value.set(value.clone()); self.cursor_position.set(cursor_pos + 1); - + // Trigger change callback if let Some(ref callback) = self.on_change { callback(&value); } - + // Validate self.validate(); } @@ -746,17 +761,17 @@ impl TextInput { fn delete_backward(&self) { let mut value = self.value.get(); let cursor_pos = self.cursor_position.get(); - + if cursor_pos > 0 && cursor_pos <= value.len() { value.remove(cursor_pos - 1); self.value.set(value.clone()); self.cursor_position.set(cursor_pos - 1); - + // Trigger change callback if let Some(ref callback) = self.on_change { callback(&value); } - + // Validate self.validate(); } @@ -766,16 +781,16 @@ impl TextInput { fn delete_forward(&self) { let mut value = self.value.get(); let cursor_pos = self.cursor_position.get(); - + if cursor_pos < value.len() { value.remove(cursor_pos); self.value.set(value.clone()); - + // Trigger change callback if let Some(ref callback) = self.on_change { callback(&value); } - + // Validate self.validate(); } @@ -814,14 +829,10 @@ impl TextInput { let bounds = self.bounds.get(); let content_bounds = self.content_bounds.get(); let style = self.style.for_state(self.state.get()); - + // Render background - batch.add_rect( - bounds, - style.background_color, - Transform::identity(), - ); - + batch.add_rect(bounds, style.background_color, Transform::identity()); + // Render text or placeholder let value = self.value.get(); let text_to_render = if value.is_empty() && !self.placeholder.is_empty() { @@ -829,13 +840,13 @@ impl TextInput { } else { &value }; - + let text_color = if value.is_empty() && !self.placeholder.is_empty() { style.placeholder_color } else { style.text_color }; - + if !text_to_render.is_empty() { let text_x = content_bounds.x; let text_y = content_bounds.y; @@ -847,13 +858,13 @@ impl TextInput { 0.0, // Default letter spacing ); } - + // Render cursor if focused if self.is_focused() && self.cursor_blink_timer.get() < 0.5 { let cursor_pos = self.cursor_position.get(); let char_width = style.font_size * 0.6; let cursor_x = content_bounds.x + (cursor_pos as f32) * char_width; - + batch.add_line( (cursor_x, content_bounds.y), (cursor_x, content_bounds.y + content_bounds.height), @@ -861,13 +872,13 @@ impl TextInput { 1.0, ); } - + // Render selection if any if let Some((start, end)) = self.get_selection() { let char_width = style.font_size * 0.6; let selection_start_x = content_bounds.x + (start as f32) * char_width; let selection_end_x = content_bounds.x + (end as f32) * char_width; - + batch.add_rect( Rect::new( selection_start_x, @@ -995,7 +1006,7 @@ impl Clone for TextInput { style: self.style.clone(), theme: self.theme.clone(), validators: Vec::new(), // Don't clone validators as they contain closures - on_change: None, // Don't clone event handlers + on_change: None, // Don't clone event handlers on_focus: None, on_blur: None, on_submit: None, @@ -1049,7 +1060,7 @@ impl TextInputBuilder { } /// Add validator - pub fn validator(mut self, validator: F) -> Self + pub fn validator(mut self, validator: F) -> Self where F: Fn(&str) -> Result<(), String> + Send + Sync + 'static, { @@ -1097,19 +1108,17 @@ mod tests { #[test] fn test_input_validation() { - let input = TextInput::new() - .required(true) - .validator(|value| { - if value.len() < 3 { - Err("Too short".to_string()) - } else { - Ok(()) - } - }); - + let input = TextInput::new().required(true).validator(|value| { + if value.len() < 3 { + Err("Too short".to_string()) + } else { + Ok(()) + } + }); + // Empty value should fail validation assert!(!input.validate()); - + // Set valid value input.set_value("test"); assert!(input.validate()); @@ -1121,7 +1130,7 @@ mod tests { .placeholder("Enter text") .required(true) .build(); - + assert_eq!(input.placeholder, "Enter text"); assert!(input.required); } diff --git a/crates/strato-widgets/src/inspector.rs b/crates/strato-widgets/src/inspector.rs index 62cf262..f8b688b 100644 --- a/crates/strato-widgets/src/inspector.rs +++ b/crates/strato-widgets/src/inspector.rs @@ -192,14 +192,14 @@ impl Widget for InspectorOverlay { fn clone_widget(&self) -> Box { // Since we can't easily clone the boxed child trait object without more bounds, - // and InspectorOverlay is likely a singleton/special widget, - // we might need a specific strategy. + // and InspectorOverlay is likely a singleton/special widget, + // we might need a specific strategy. // For now, assuming `InspectorOverlay` needs to be Clone but `child` is `Box`. // `Box` isn't automatically cloneable unless `Widget` has `clone_widget`. // We can use the child's `clone_widget` method. Box::new(Self { - id: generate_id(), // Generate new ID on clone? Or copy? Usually clone implies new ID for widgets or copy? - // BaseWidget generates new ID. + id: generate_id(), // Generate new ID on clone? Or copy? Usually clone implies new ID for widgets or copy? + // BaseWidget generates new ID. child: self.child.clone_widget(), shortcut: self.shortcut, visible: self.visible, diff --git a/crates/strato-widgets/src/layout.rs b/crates/strato-widgets/src/layout.rs index 1539693..e9412fb 100644 --- a/crates/strato-widgets/src/layout.rs +++ b/crates/strato-widgets/src/layout.rs @@ -1,12 +1,15 @@ //! Layout widgets for arranging child widgets -use crate::widget::{Widget, WidgetId, generate_id}; +use crate::widget::{generate_id, Widget, WidgetId}; +use std::any::Any; use strato_core::{ event::{Event, EventResult}, - layout::{Constraints, Layout, Size, FlexItem, FlexContainer, FlexDirection, JustifyContent, AlignItems}, + layout::{ + AlignItems, Constraints, FlexContainer, FlexDirection, FlexItem, JustifyContent, Layout, + Size, + }, }; use strato_renderer::batch::RenderBatch; -use std::any::Any; /// Main axis alignment for flex layouts #[derive(Debug, Clone, Copy, PartialEq)] @@ -92,7 +95,7 @@ impl Widget for Row { fn layout(&mut self, constraints: Constraints) -> Size { let engine = strato_core::layout::LayoutEngine::new(); - + // Relax constraints for children measurement let child_constraints = Constraints { min_width: 0.0, @@ -100,14 +103,14 @@ impl Widget for Row { min_height: 0.0, max_height: constraints.max_height, }; - + // Calculate child sizes let mut child_data = Vec::new(); let mut sizes = Vec::with_capacity(self.children.len()); for child in &mut self.children { let child_size = child.layout(child_constraints); sizes.push(child_size); - + let mut flex_item = FlexItem::default(); if let Some(flex) = child.as_any().downcast_ref::() { flex_item = FlexItem::grow(flex.flex); @@ -116,7 +119,7 @@ impl Widget for Row { } // Cache sizes for use during render() self.cached_child_sizes = sizes; - + // Calculate layout let container = FlexContainer { direction: FlexDirection::Row, @@ -138,23 +141,25 @@ impl Widget for Row { ..Default::default() }; let layouts = engine.calculate_flex_layout(&container, &child_data, constraints); - + // Calculate total size - let width = layouts.iter() + let width = layouts + .iter() .map(|l| l.position.x + l.size.width) .max_by(|a, b| a.partial_cmp(b).unwrap()) .unwrap_or(0.0); - let height = layouts.iter() + let height = layouts + .iter() .map(|l| l.size.height) .max_by(|a, b| a.partial_cmp(b).unwrap()) .unwrap_or(0.0); - + Size::new(width, height) } fn render(&self, batch: &mut RenderBatch, layout: Layout) { let engine = strato_core::layout::LayoutEngine::new(); - + // Calculate child layouts using cached sizes measured in layout() let mut child_data = Vec::new(); for (i, child) in self.children.iter().enumerate() { @@ -163,14 +168,14 @@ impl Widget for Row { .get(i) .copied() .unwrap_or_else(|| Size::new(100.0, 50.0)); - + let mut flex_item = FlexItem::default(); if let Some(flex) = child.as_any().downcast_ref::() { flex_item = FlexItem::grow(flex.flex); } child_data.push((flex_item, child_size)); } - + let container = FlexContainer { direction: FlexDirection::Row, justify_content: match self.main_axis_alignment { @@ -190,14 +195,16 @@ impl Widget for Row { }, ..Default::default() }; - let layouts = engine.calculate_flex_layout(&container, &child_data, Constraints::loose(layout.size.width, layout.size.height)); - + let layouts = engine.calculate_flex_layout( + &container, + &child_data, + Constraints::loose(layout.size.width, layout.size.height), + ); + // Render children for (child, child_layout) in self.children.iter().zip(layouts.iter()) { - let absolute_layout = Layout::new( - layout.position + child_layout.position, - child_layout.size, - ); + let absolute_layout = + Layout::new(layout.position + child_layout.position, child_layout.size); child.render(batch, absolute_layout); } } @@ -305,7 +312,7 @@ impl Widget for Column { fn layout(&mut self, constraints: Constraints) -> Size { let engine = strato_core::layout::LayoutEngine::new(); - + // Relax constraints for children measurement let child_constraints = Constraints { min_width: 0.0, @@ -313,14 +320,14 @@ impl Widget for Column { min_height: 0.0, max_height: constraints.max_height, }; - + // Calculate child sizes let mut child_data = Vec::new(); let mut sizes = Vec::with_capacity(self.children.len()); for child in &mut self.children { let child_size = child.layout(child_constraints); sizes.push(child_size); - + let mut flex_item = FlexItem::default(); if let Some(flex) = child.as_any().downcast_ref::() { flex_item = FlexItem::grow(flex.flex); @@ -329,7 +336,7 @@ impl Widget for Column { } // Cache sizes for render() self.cached_child_sizes = sizes; - + // Calculate layout let container = FlexContainer { direction: FlexDirection::Column, @@ -351,23 +358,25 @@ impl Widget for Column { ..Default::default() }; let layouts = engine.calculate_flex_layout(&container, &child_data, constraints); - + // Calculate total size - let width = layouts.iter() + let width = layouts + .iter() .map(|l| l.size.width) .max_by(|a, b| a.partial_cmp(b).unwrap()) .unwrap_or(0.0); - let height = layouts.iter() + let height = layouts + .iter() .map(|l| l.position.y + l.size.height) .max_by(|a, b| a.partial_cmp(b).unwrap()) .unwrap_or(0.0); - + Size::new(width, height) } fn render(&self, batch: &mut RenderBatch, layout: Layout) { let engine = strato_core::layout::LayoutEngine::new(); - + // Calculate child layouts using cached sizes computed during layout() let mut child_data = Vec::new(); for (i, child) in self.children.iter().enumerate() { @@ -376,14 +385,14 @@ impl Widget for Column { .get(i) .copied() .unwrap_or_else(|| Size::new(100.0, 50.0)); - + let mut flex_item = FlexItem::default(); if let Some(flex) = child.as_any().downcast_ref::() { flex_item = FlexItem::grow(flex.flex); } child_data.push((flex_item, child_size)); } - + let container = FlexContainer { direction: FlexDirection::Column, justify_content: match self.main_axis_alignment { @@ -403,14 +412,16 @@ impl Widget for Column { }, ..Default::default() }; - let layouts = engine.calculate_flex_layout(&container, &child_data, Constraints::loose(layout.size.width, layout.size.height)); - + let layouts = engine.calculate_flex_layout( + &container, + &child_data, + Constraints::loose(layout.size.width, layout.size.height), + ); + // Render children for (child, child_layout) in self.children.iter().zip(layouts.iter()) { - let absolute_layout = Layout::new( - layout.position + child_layout.position, - child_layout.size, - ); + let absolute_layout = + Layout::new(layout.position + child_layout.position, child_layout.size); child.render(batch, absolute_layout); } } @@ -492,13 +503,13 @@ impl Widget for Stack { fn layout(&mut self, constraints: Constraints) -> Size { let mut max_width: f32 = 0.0; let mut max_height: f32 = 0.0; - + for child in &mut self.children { let size = child.layout(constraints); max_width = max_width.max(size.width); max_height = max_height.max(size.height); } - + Size::new(max_width, max_height) } diff --git a/crates/strato-widgets/src/lib.rs b/crates/strato-widgets/src/lib.rs index 3d2f2b9..fe5727e 100644 --- a/crates/strato-widgets/src/lib.rs +++ b/crates/strato-widgets/src/lib.rs @@ -8,6 +8,7 @@ pub mod builder; pub mod button; pub mod checkbox; pub mod container; +pub mod control; pub mod dropdown; pub mod grid; pub mod image; @@ -19,9 +20,9 @@ pub mod scroll_view; pub mod slider; pub mod text; pub mod theme; +pub mod top_bar; pub mod widget; pub mod wrap; -pub mod top_bar; pub mod prelude; use crate::prelude::*; @@ -31,6 +32,7 @@ pub use builder::WidgetBuilder; pub use button::{Button, ButtonStyle}; pub use checkbox::{Checkbox, CheckboxStyle, RadioButton}; pub use container::{Container, ContainerStyle}; +pub use control::{ControlRole, ControlSemantics, ControlState}; pub use dropdown::{Dropdown, DropdownOption, DropdownStyle}; pub use grid::{Grid, GridUnit}; pub use image::{ @@ -44,8 +46,8 @@ pub use slider::{ProgressBar, Slider, SliderStyle}; pub use strato_macros::view; pub use text::{Text, TextStyle}; pub use theme::Theme; -pub use widget::{Widget, WidgetContext, WidgetId}; pub use top_bar::TopBar; +pub use widget::{Widget, WidgetContext, WidgetId}; /// Initialize the widgets module pub fn init() -> strato_core::Result<()> { diff --git a/crates/strato-widgets/src/prelude.rs b/crates/strato-widgets/src/prelude.rs index 35fc710..a485539 100644 --- a/crates/strato-widgets/src/prelude.rs +++ b/crates/strato-widgets/src/prelude.rs @@ -1,10 +1,11 @@ //! Prelude module for StratoUI widgets -//! +//! //! This module re-exports the most commonly used types and traits from the widgets crate, //! allowing users to import everything they need with a single `use strato_widgets::prelude::*;` // Animation pub use crate::animation::{AnimationController, Curve, Tween, Tweenable}; +pub use crate::control::{ControlRole, ControlSemantics, ControlState}; // Re-export core types that are commonly used with widgets pub use strato_core::prelude::*; @@ -15,18 +16,18 @@ pub use crate::widget::{Widget, WidgetId, WidgetState}; // Layout widgets pub use crate::container::Container; -pub use crate::layout::{Column, Row, MainAxisAlignment, CrossAxisAlignment}; +pub use crate::grid::{Grid, GridUnit}; +pub use crate::layout::{Column, CrossAxisAlignment, MainAxisAlignment, Row}; pub use crate::scroll_view::ScrollView; pub use crate::wrap::{Wrap, WrapAlignment, WrapCrossAlignment}; -pub use crate::grid::{Grid, GridUnit}; // Basic widgets -pub use crate::text::Text; pub use crate::button::{Button, ButtonStyle}; pub use crate::input::TextInput; +pub use crate::text::Text; // Theme system -pub use crate::theme::{Theme, ColorPalette, Typography}; +pub use crate::theme::{ColorPalette, Theme, Typography}; // Common layout types from strato-core -pub use strato_core::layout::EdgeInsets; \ No newline at end of file +pub use strato_core::layout::EdgeInsets; diff --git a/crates/strato-widgets/src/registry.rs b/crates/strato-widgets/src/registry.rs index 4b5bd63..7e423f1 100644 --- a/crates/strato-widgets/src/registry.rs +++ b/crates/strato-widgets/src/registry.rs @@ -1,16 +1,18 @@ +use crate::image::{Image, ImageFit, ImageSource}; +use crate::prelude::*; +use crate::widget::{Widget, WidgetContext, WidgetId}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use strato_core::ui_node::{UiNode, WidgetNode, PropValue}; -use crate::widget::{Widget, WidgetContext, WidgetId}; use strato_core::event::{Event, EventResult}; use strato_core::layout::{Constraints, Layout, Size}; use strato_core::types::Point; +use strato_core::ui_node::{PropValue, UiNode, WidgetNode}; use strato_renderer::batch::RenderBatch; -use crate::prelude::*; -use crate::image::{Image, ImageSource, ImageFit}; /// A builder function that creates a widget from properties. -type WidgetBuilder = Box, Vec, &WidgetRegistry) -> Box + Send + Sync>; +type WidgetBuilder = Box< + dyn Fn(Vec<(String, PropValue)>, Vec, &WidgetRegistry) -> Box + Send + Sync, +>; /// Registry for mapping widget names to their constructors. pub struct WidgetRegistry { @@ -22,34 +24,44 @@ pub struct WidgetRegistry { pub struct BoxedWidget(pub Box); impl Widget for BoxedWidget { - fn id(&self) -> WidgetId { self.0.id() } - + fn id(&self) -> WidgetId { + self.0.id() + } + fn layout(&mut self, constraints: Constraints) -> Size { self.0.layout(constraints) } - + fn render(&self, batch: &mut RenderBatch, layout: Layout) { self.0.render(batch, layout) } - + fn handle_event(&mut self, event: &Event) -> EventResult { self.0.handle_event(event) } - + fn update(&mut self, ctx: &WidgetContext) { self.0.update(ctx) } - - fn children(&self) -> Vec<&(dyn Widget + '_)> { self.0.children() } - fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> { self.0.children_mut() } - + + fn children(&self) -> Vec<&(dyn Widget + '_)> { + self.0.children() + } + fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> { + self.0.children_mut() + } + fn hit_test(&self, point: Point, layout: Layout) -> bool { self.0.hit_test(point, layout) } - - fn as_any(&self) -> &dyn std::any::Any { self.0.as_any() } - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self.0.as_any_mut() } - + + fn as_any(&self) -> &dyn std::any::Any { + self.0.as_any() + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self.0.as_any_mut() + } + fn clone_widget(&self) -> Box { self.0.clone_widget() } @@ -67,7 +79,10 @@ impl WidgetRegistry { /// Register a widget builder. pub fn register(&mut self, name: &str, builder: F) where - F: Fn(Vec<(String, PropValue)>, Vec, &WidgetRegistry) -> Box + Send + Sync + 'static, + F: Fn(Vec<(String, PropValue)>, Vec, &WidgetRegistry) -> Box + + Send + + Sync + + 'static, { self.builders.insert(name.to_string(), Box::new(builder)); } @@ -78,15 +93,15 @@ impl WidgetRegistry { UiNode::Widget(node) => self.build_widget(node), UiNode::Text(text) => Box::new(Text::new(text)), UiNode::Fragment(children) => { - let mut col = Column::new(); - for child in children { - col = col.child(self.build(child).0); - } - Box::new(col) + let mut col = Column::new(); + for child in children { + col = col.child(self.build(child).0); + } + Box::new(col) } }) } - + fn build_widget(&self, node: WidgetNode) -> Box { if let Some(builder) = self.builders.get(&node.name) { builder(node.props, node.children, self) @@ -103,15 +118,15 @@ impl WidgetRegistry { for (name, value) in props { match (name.as_str(), value) { ("padding", PropValue::Float(v)) => widget = widget.padding(v as f32), - ("background", PropValue::Color(c)) => widget = widget.background(c), - ("width", PropValue::Float(v)) => widget = widget.width(v as f32), - ("height", PropValue::Float(v)) => widget = widget.height(v as f32), - ("radius", PropValue::Float(v)) => widget = widget.border_radius(v as f32), - ("margin", PropValue::Float(v)) => widget = widget.margin(v as f32), + ("background", PropValue::Color(c)) => widget = widget.background(c), + ("width", PropValue::Float(v)) => widget = widget.width(v as f32), + ("height", PropValue::Float(v)) => widget = widget.height(v as f32), + ("radius", PropValue::Float(v)) => widget = widget.border_radius(v as f32), + ("margin", PropValue::Float(v)) => widget = widget.margin(v as f32), _ => {} } } - // Handle "child" logic (Container takes 1 child usually, but our generic AST has list) + // Handle "child" logic (Container takes 1 child usually, but our generic AST has list) if let Some(first) = children.first() { widget = widget.child(registry.build(first.clone())); } @@ -128,13 +143,14 @@ impl WidgetRegistry { } } } - // Column::children takes Vec>. - // registry.build returns BoxedWidget. + // Column::children takes Vec>. + // registry.build returns BoxedWidget. // We need to unwrap or map. - let child_widgets: Vec> = children.into_iter() + let child_widgets: Vec> = children + .into_iter() .map(|child| registry.build(child).0) .collect(); - + widget = widget.children(child_widgets); Box::new(widget) }); @@ -143,13 +159,14 @@ impl WidgetRegistry { self.register("Row", |props, children, registry| { let mut widget = Row::new(); for (name, value) in props { - if name == "spacing" { + if name == "spacing" { if let PropValue::Float(v) = value { widget = widget.spacing(v as f32); } } } - let child_widgets: Vec> = children.into_iter() + let child_widgets: Vec> = children + .into_iter() .map(|child| registry.build(child).0) .collect(); widget = widget.children(child_widgets); @@ -159,26 +176,26 @@ impl WidgetRegistry { // Text self.register("Text", |props, _children, _registry| { let mut text = String::new(); - + // First pass: find text semantic prop for (name, value) in &props { - if name == "text" { + if name == "text" { if let PropValue::String(s) = value { text = s.clone(); } - } + } } - + let mut widget = Text::new(text); - + // Second pass: apply properties - for (name, value) in props { + for (name, value) in props { match (name.as_str(), value) { ("color", PropValue::Color(c)) => widget = widget.color(c), - ("size", PropValue::Float(v)) => widget = widget.size(v as f32), + ("size", PropValue::Float(v)) => widget = widget.size(v as f32), _ => {} } - } + } Box::new(widget) }); @@ -186,26 +203,26 @@ impl WidgetRegistry { // Button self.register("Button", |props, _children, _registry| { let mut label = String::new(); - for (name, value) in &props { + for (name, value) in &props { if name == "text" { if let PropValue::String(s) = value { label = s.clone(); } } } - + let widget = Button::new(label); - + // Button usually doesn't take children in this framework, just text in constructor? // But macro might support `Button { child: Icon }`? // Existing Button::new implementation takes string. // If children present, ignored? or fallback? - + for (name, value) in props { - match (name.as_str(), value) { - // disabled? + match (name.as_str(), value) { + // disabled? - // events? + // events? _ => {} } } @@ -213,30 +230,30 @@ impl WidgetRegistry { }); // Image self.register("Image", |props, _children, _registry| { - let mut source = ImageSource::Placeholder { - width: 100, - height: 100, - color: Color::GRAY + let mut source = ImageSource::Placeholder { + width: 100, + height: 100, + color: Color::GRAY, }; - + for (name, value) in &props { if name == "source" { if let PropValue::String(s) = value { // Simple heuristic for source type if s.starts_with("http") { - source = ImageSource::Url(s.clone()); + source = ImageSource::Url(s.clone()); } else if s.starts_with("placeholder") { // Format: placeholder:width:height:hex // Simplified parsing for now: placeholder -> default } else { - source = ImageSource::File(std::path::PathBuf::from(s)); + source = ImageSource::File(std::path::PathBuf::from(s)); } } } } - + let mut widget = Image::new(source); - for (name, value) in props { + for (name, value) in props { match (name.as_str(), value) { ("fit", PropValue::String(s)) => { let fit = match s.as_str() { @@ -248,11 +265,11 @@ impl WidgetRegistry { widget = widget.fit(fit); } ("opacity", PropValue::Float(v)) => widget = widget.opacity(v as f32), - ("radius", PropValue::Float(v)) => widget = widget.border_radius(v as f32), + ("radius", PropValue::Float(v)) => widget = widget.border_radius(v as f32), _ => {} } - } - Box::new(widget) + } + Box::new(widget) }); // TopBar @@ -268,11 +285,11 @@ impl WidgetRegistry { } let mut widget = crate::top_bar::TopBar::new(title); - + for (name, value) in props { match (name.as_str(), value) { ("background", PropValue::Color(c)) => widget = widget.with_background(c), - // height handles directly field access if needed, or via method if added + // height handles directly field access if needed, or via method if added _ => {} } } diff --git a/crates/strato-widgets/src/scroll_view.rs b/crates/strato-widgets/src/scroll_view.rs index 4bcbfc2..caa94aa 100644 --- a/crates/strato-widgets/src/scroll_view.rs +++ b/crates/strato-widgets/src/scroll_view.rs @@ -1,8 +1,8 @@ use crate::prelude::*; use strato_core::event::{Event, EventResult, MouseEvent}; use strato_core::layout::{Constraints, Layout, Size}; +use strato_core::types::{Color, Point, Rect, Transform}; use strato_renderer::batch::RenderBatch; -use strato_core::types::{Rect, Point, Transform, Color}; use crate::widget::BaseWidget; @@ -13,7 +13,7 @@ pub struct ScrollView { offset: Point, content_size: Size, viewport_size: Size, - + // Interaction state bounds: strato_core::state::Signal, scrollbar_rect: strato_core::state::Signal, @@ -38,7 +38,13 @@ impl ScrollView { } } - fn update_scrollbar_rect(&self, content_height: f32, viewport_height: f32, offset_y: f32, bounds: Rect) { + fn update_scrollbar_rect( + &self, + content_height: f32, + viewport_height: f32, + offset_y: f32, + bounds: Rect, + ) { if content_height > viewport_height { let ratio = viewport_height / content_height; let thumb_height = (viewport_height * ratio).max(20.0); @@ -49,12 +55,17 @@ impl ScrollView { } else { 0.0 }; - + let scrollbar_width = 10.0; let scrollbar_x = bounds.x + bounds.width - scrollbar_width; let scrollbar_y = bounds.y + thumb_y; - - self.scrollbar_rect.set(Rect::new(scrollbar_x, scrollbar_y, scrollbar_width, thumb_height)); + + self.scrollbar_rect.set(Rect::new( + scrollbar_x, + scrollbar_y, + scrollbar_width, + thumb_height, + )); } else { self.scrollbar_rect.set(Rect::new(0.0, 0.0, 0.0, 0.0)); } @@ -68,12 +79,9 @@ impl Widget for ScrollView { fn layout(&mut self, constraints: Constraints) -> Size { // ScrollView takes all available space or respects max size - let self_size = Size::new( - constraints.max_width, - constraints.max_height - ); + let self_size = Size::new(constraints.max_width, constraints.max_height); self.viewport_size = self_size; - + // Layout child with infinite constraints let child_constraints = Constraints { min_width: 0.0, @@ -83,37 +91,51 @@ impl Widget for ScrollView { }; self.content_size = self.child.layout(child_constraints); - + self_size } fn render(&self, batch: &mut RenderBatch, layout: Layout) { - let bounds = Rect::new(layout.position.x, layout.position.y, layout.size.width, layout.size.height); + let bounds = Rect::new( + layout.position.x, + layout.position.y, + layout.size.width, + layout.size.height, + ); self.bounds.set(bounds); - + // Update scrollbar rect - self.update_scrollbar_rect(self.content_size.height, layout.size.height, self.offset.y, bounds); + self.update_scrollbar_rect( + self.content_size.height, + layout.size.height, + self.offset.y, + bounds, + ); // 1. Push Clip batch.push_clip(bounds); // 2. Render child offset let draw_pos = layout.position - self.offset.to_vec2(); - + // We use the computed content size for the child layout let child_layout = Layout::new(draw_pos, self.content_size); self.child.render(batch, child_layout); // 3. Pop Clip batch.pop_clip(); - + // 4. Draw Scrollbar let scrollbar = self.scrollbar_rect.get(); if scrollbar.width > 0.0 { - // Draw thumb + // Draw thumb batch.add_rect( scrollbar, - if self.is_dragging { Color::rgba(0.4, 0.4, 0.4, 0.8) } else { Color::rgba(0.5, 0.5, 0.5, 0.5) }, + if self.is_dragging { + Color::rgba(0.4, 0.4, 0.4, 0.8) + } else { + Color::rgba(0.5, 0.5, 0.5, 0.5) + }, Transform::identity(), ); } @@ -124,42 +146,42 @@ impl Widget for ScrollView { Event::MouseWheel { delta, .. } => { let delta_x = delta.x; let delta_y = delta.y; - + let viewport_w = self.viewport_size.width; let viewport_h = self.viewport_size.height; - + let max_x = (self.content_size.width - viewport_w).max(0.0); let max_y = (self.content_size.height - viewport_h).max(0.0); - + self.offset.x = (self.offset.x - delta_x).clamp(0.0, max_x); self.offset.y = (self.offset.y - delta_y).clamp(0.0, max_y); - + // Update scrollbar rect immediately for responsiveness if we were running a single loop // but render will handle it. - + EventResult::Handled } Event::MouseDown(mouse) => { let point = Point::new(mouse.position.x, mouse.position.y); let scrollbar = self.scrollbar_rect.get(); - + if scrollbar.contains(point) { self.is_dragging = true; self.drag_start_y = point.y; self.offset_start_y = self.offset.y; return EventResult::Handled; } - + self.child.handle_event(event) } Event::MouseMove(mouse) => { if self.is_dragging { let point = Point::new(mouse.position.x, mouse.position.y); let delta_y = point.y - self.drag_start_y; - + let viewport_h = self.viewport_size.height; let content_h = self.content_size.height; - + if content_h > viewport_h { // Calculate how much offset changes per pixel of scrollbar movement let track_height = viewport_h; @@ -167,15 +189,16 @@ impl Widget for ScrollView { let ratio = viewport_h / content_h; let thumb_height = (viewport_h * ratio).max(20.0); let track_range = track_height - thumb_height; - + if track_range > 0.0 { let max_offset = content_h - viewport_h; let offset_delta = (delta_y / track_range) * max_offset; - - self.offset.y = (self.offset_start_y + offset_delta).clamp(0.0, max_offset); + + self.offset.y = + (self.offset_start_y + offset_delta).clamp(0.0, max_offset); } } - + return EventResult::Handled; } self.child.handle_event(event) @@ -213,7 +236,7 @@ impl Widget for ScrollView { offset_start_y: 0.0, }) } - + fn children(&self) -> Vec<&(dyn Widget + '_)> { vec![self.child.as_ref()] } diff --git a/crates/strato-widgets/src/slider.rs b/crates/strato-widgets/src/slider.rs index b56f1cb..804d3ef 100644 --- a/crates/strato-widgets/src/slider.rs +++ b/crates/strato-widgets/src/slider.rs @@ -1,12 +1,13 @@ //! Slider and Progress widgets implementation for StratoUI +use crate::control::{ControlRole, ControlState}; +use crate::widget::{generate_id, Widget, WidgetContext, WidgetId, WidgetState}; use std::any::Any; -use crate::widget::{Widget, WidgetId, generate_id}; use strato_core::{ event::{Event, EventResult, MouseButton}, - layout::{Size, Constraints, Layout}, + layout::{Constraints, Layout, Size}, state::Signal, - types::{Point, Rect, Color, Transform}, + types::{Color, Point, Rect, Transform}, }; use strato_renderer::batch::RenderBatch; @@ -24,6 +25,7 @@ pub struct Slider { style: SliderStyle, dragging: Signal, bounds: Signal, + control: ControlState, } /// Styling options for slider @@ -45,20 +47,31 @@ impl Default for SliderStyle { Self { track_height: 4.0, thumb_size: 20.0, - track_color: [0.8, 0.8, 0.8, 1.0], // Light gray + track_color: [0.8, 0.8, 0.8, 1.0], // Light gray track_fill_color: [0.2, 0.6, 1.0, 1.0], // Blue - thumb_color: [1.0, 1.0, 1.0, 1.0], // White + thumb_color: [1.0, 1.0, 1.0, 1.0], // White thumb_hover_color: [0.95, 0.95, 0.95, 1.0], // Light gray thumb_active_color: [0.9, 0.9, 0.9, 1.0], // Darker gray - disabled_color: [0.7, 0.7, 0.7, 1.0], // Gray + disabled_color: [0.7, 0.7, 0.7, 1.0], // Gray border_radius: 2.0, } } } +fn color_from(values: [f32; 4]) -> Color { + Color::rgba(values[0], values[1], values[2], values[3]) +} + +fn blend_color(a: Color, b: Color, t: f32) -> Color { + let mix = |from: f32, to: f32| from + (to - from) * t; + Color::rgba(mix(a.r, b.r), mix(a.g, b.g), mix(a.b, b.b), mix(a.a, b.a)) +} + impl Slider { /// Create a new slider pub fn new(min: f32, max: f32) -> Self { + let mut control = ControlState::new(ControlRole::Slider); + control.set_value(format!("{}", min)); Self { id: generate_id(), value: Signal::new(min), @@ -71,6 +84,7 @@ impl Slider { style: SliderStyle::default(), dragging: Signal::new(false), bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), + control, } } @@ -78,6 +92,7 @@ impl Slider { pub fn value(mut self, value: f32) -> Self { let clamped = value.clamp(self.min, self.max); self.value.set(clamped); + self.control.set_value(format!("{:.2}", clamped)); self } @@ -97,6 +112,7 @@ impl Slider { /// Set enabled state pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; + self.control.set_disabled(!enabled); self } @@ -117,7 +133,7 @@ impl Slider { } /// Set the value - pub fn set_value(&self, value: f32) { + pub fn set_value(&mut self, value: f32) { let clamped = value.clamp(self.min, self.max); let stepped = if self.step > 0.0 { (clamped / self.step).round() * self.step @@ -125,13 +141,14 @@ impl Slider { clamped }; self.value.set(stepped); + self.control.set_value(format!("{:.2}", stepped)); } /// Calculate value from position fn value_from_position(&self, x: f32, track_width: f32) -> f32 { let ratio = (x / track_width).clamp(0.0, 1.0); let value = self.min + ratio * (self.max - self.min); - + if self.step > 0.0 { (value / self.step).round() * self.step } else { @@ -150,7 +167,7 @@ impl Slider { } /// Handle mouse events using stored bounds - fn handle_mouse_event(&self, event: &Event) -> EventResult { + fn handle_mouse_event(&mut self, event: &Event) -> EventResult { if !self.enabled { return EventResult::Ignored; } @@ -165,8 +182,9 @@ impl Slider { if !bounds.contains(point) { return EventResult::Ignored; } - + if let Some(MouseButton::Left) = mouse_event.button { + self.control.press(point, bounds); let local_x = mouse_event.position.x - track_start_x; let new_value = self.value_from_position(local_x, track_width); self.set_value(new_value); @@ -180,11 +198,19 @@ impl Slider { let local_x = mouse_event.position.x - track_start_x; let new_value = self.value_from_position(local_x, track_width); self.set_value(new_value); + self.control.set_state(WidgetState::Pressed); EventResult::Handled } + Event::MouseMove(mouse_event) => { + let point = Point::new(mouse_event.position.x, mouse_event.position.y); + self.control.hover(bounds.contains(point)); + EventResult::Ignored + } Event::MouseUp(mouse_event) => { if let Some(MouseButton::Left) = mouse_event.button { self.dragging.set(false); + let point = Point::new(mouse_event.position.x, mouse_event.position.y); + self.control.release(point, bounds); EventResult::Handled } else { EventResult::Ignored @@ -219,34 +245,29 @@ impl Widget for Slider { ); self.bounds.set(bounds); - if !self.enabled { - let disabled_color = Color::rgba( - self.style.disabled_color[0], - self.style.disabled_color[1], - self.style.disabled_color[2], - self.style.disabled_color[3], - ); - batch.add_rect(bounds, disabled_color, Transform::identity()); - return; - } - let track_width = (bounds.width - self.style.thumb_size).max(0.0); let track_x = bounds.x + self.style.thumb_size * 0.5; let track_y = bounds.y + (bounds.height - self.style.track_height) * 0.5; - let track_rect = Rect::new( - track_x, - track_y, - track_width, - self.style.track_height, - ); + let track_rect = Rect::new(track_x, track_y, track_width, self.style.track_height); - let track_color = Color::rgba( - self.style.track_color[0], - self.style.track_color[1], - self.style.track_color[2], - self.style.track_color[3], - ); + let state = if !self.enabled { + WidgetState::Disabled + } else if self.dragging.get() { + WidgetState::Pressed + } else { + self.control.state() + }; + let interaction = if self.dragging.get() { + 1.0 + } else { + self.control.interaction_factor() + }; + + let track_color = match state { + WidgetState::Disabled => color_from(self.style.disabled_color), + _ => color_from(self.style.track_color), + }; batch.add_rect(track_rect, track_color, Transform::identity()); @@ -258,12 +279,10 @@ impl Widget for Slider { self.style.track_height, ); - let fill_color = Color::rgba( - self.style.track_fill_color[0], - self.style.track_fill_color[1], - self.style.track_fill_color[2], - self.style.track_fill_color[3], - ); + let mut fill_color = color_from(self.style.track_fill_color); + if matches!(state, WidgetState::Disabled) { + fill_color = blend_color(fill_color, track_color, 0.6); + } batch.add_rect(fill_rect, fill_color, Transform::identity()); @@ -271,12 +290,14 @@ impl Widget for Slider { let thumb_center_y = bounds.y + bounds.height * 0.5; let thumb_radius = self.style.thumb_size * 0.5; - let thumb_color = Color::rgba( - self.style.thumb_color[0], - self.style.thumb_color[1], - self.style.thumb_color[2], - self.style.thumb_color[3], - ); + let thumb_base = color_from(self.style.thumb_color); + let thumb_target = match state { + WidgetState::Pressed => color_from(self.style.thumb_active_color), + WidgetState::Hovered => color_from(self.style.thumb_hover_color), + WidgetState::Disabled => color_from(self.style.disabled_color), + _ => thumb_base, + }; + let thumb_color = blend_color(thumb_base, thumb_target, interaction); batch.add_circle( (thumb_center_x, thumb_center_y), @@ -288,12 +309,25 @@ impl Widget for Slider { } fn handle_event(&mut self, event: &Event) -> EventResult { - match event { - Event::MouseDown(_) | Event::MouseUp(_) | Event::MouseMove(_) => { - self.handle_mouse_event(event) + if matches!( + event, + Event::MouseDown(_) | Event::MouseUp(_) | Event::MouseMove(_) + ) { + let result = self.handle_mouse_event(event); + if let EventResult::Handled = result { + return result; } - _ => EventResult::Ignored, } + + if let EventResult::Handled = self.control.handle_keyboard_activation(event) { + return EventResult::Handled; + } + + EventResult::Ignored + } + + fn update(&mut self, ctx: &WidgetContext) { + self.control.update(ctx.delta_time); } fn as_any(&self) -> &dyn Any { @@ -337,7 +371,7 @@ impl Default for ProgressStyle { fn default() -> Self { Self { background_color: [0.9, 0.9, 0.9, 1.0], // Light gray - fill_color: [0.2, 0.6, 1.0, 1.0], // Blue + fill_color: [0.2, 0.6, 1.0, 1.0], // Blue border_radius: 4.0, border_width: 1.0, border_color: [0.8, 0.8, 0.8, 1.0], // Gray @@ -492,22 +526,22 @@ mod tests { #[test] fn test_slider_value_clamping() { - let slider = Slider::new(0.0, 100.0); - + let mut slider = Slider::new(0.0, 100.0); + slider.set_value(150.0); assert_eq!(slider.get_value(), 100.0); - + slider.set_value(-50.0); assert_eq!(slider.get_value(), 0.0); } #[test] fn test_slider_step() { - let slider = Slider::new(0.0, 100.0).step(10.0); - + let mut slider = Slider::new(0.0, 100.0).step(10.0); + slider.set_value(23.0); assert_eq!(slider.get_value(), 20.0); // Rounded to nearest step - + slider.set_value(27.0); assert_eq!(slider.get_value(), 30.0); } @@ -523,13 +557,13 @@ mod tests { #[test] fn test_progress_bar_progress() { let progress = ProgressBar::new(100.0); - + progress.set_value(50.0); assert_eq!(progress.progress(), 0.5); - + progress.set_value(100.0); assert_eq!(progress.progress(), 1.0); - + progress.set_value(150.0); // Should clamp assert_eq!(progress.progress(), 1.0); } diff --git a/crates/strato-widgets/src/text.rs b/crates/strato-widgets/src/text.rs index 08de50c..b333a68 100644 --- a/crates/strato-widgets/src/text.rs +++ b/crates/strato-widgets/src/text.rs @@ -1,21 +1,19 @@ //! Text widget implementation -//! +//! //! Provides text display components with various styles, formatting, and layout options. +use crate::widget::{generate_id, Widget, WidgetId}; +use std::{any::Any, sync::Arc, sync::OnceLock}; use strato_core::{ - layout::{Size, Constraints, Layout}, - types::{Rect, Color, Point}, - state::{Signal}, - theme::{Theme}, event::{Event, EventResult}, + layout::{Constraints, Layout, Size}, + state::Signal, + theme::Theme, + types::{Color, Point, Rect}, }; use strato_renderer::{ - vertex::VertexBuilder, - batch::RenderBatch, - gpu::texture_mgr::GlyphRasterizer, + batch::RenderBatch, gpu::texture_mgr::GlyphRasterizer, vertex::VertexBuilder, }; -use crate::widget::{Widget, WidgetId, generate_id}; -use std::{sync::Arc, any::Any, sync::OnceLock}; // Helper for text measurement fn get_rasterizer() -> &'static GlyphRasterizer { @@ -51,7 +49,6 @@ fn measure_line_width(line: &str, font_size: f32, letter_spacing: f32) -> f32 { width } - /// Text alignment options #[derive(Debug, Clone, Copy, PartialEq)] pub enum TextAlign { @@ -151,16 +148,16 @@ impl Default for TextStyle { // Use platform-specific default fonts instead of generic "system-ui" #[cfg(target_os = "windows")] let default_family = "Segoe UI"; - + #[cfg(target_os = "macos")] let default_family = "SF Pro Display"; - + #[cfg(target_os = "linux")] let default_family = "Ubuntu"; - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let default_family = "Arial"; - + Self { font_family: default_family.to_string(), font_size: 14.0, @@ -471,21 +468,24 @@ impl Text { pub fn measure_text(&self, available_width: f32) -> Size { // Accurate text measurement let line_height = self.style.font_size * self.style.line_height; - + let mut lines = Vec::new(); let mut current_line = String::new(); let mut current_line_width = 0.0; - + let content = self.content.get(); let words: Vec<&str> = content.split_whitespace().collect(); let space_width = measure_char_width(' ', self.style.font_size) + self.style.letter_spacing; - + for (i, word) in words.iter().enumerate() { - let word_width = measure_line_width(word, self.style.font_size, self.style.letter_spacing); - + let word_width = + measure_line_width(word, self.style.font_size, self.style.letter_spacing); + // Check if adding this word would exceed available width // (Only if we already have content on this line) - if !current_line.is_empty() && current_line_width + space_width + word_width > available_width { + if !current_line.is_empty() + && current_line_width + space_width + word_width > available_width + { // Push current line and start new one lines.push(current_line); current_line = String::from(*word); @@ -499,14 +499,14 @@ impl Text { current_line_width += word_width; } } - + if !current_line.is_empty() { lines.push(current_line); } - + // If no content but we have text, treat as one line (e.g. single word too long or empty) if lines.is_empty() && !content.is_empty() { - lines.push(content.clone()); + lines.push(content.clone()); } // Apply max_lines constraint @@ -515,23 +515,26 @@ impl Text { lines.truncate(max_lines); } } - + let width = if lines.is_empty() { 0.0 } else { // Calculate max width of lines - lines.iter() - .map(|line| measure_line_width(line, self.style.font_size, self.style.letter_spacing)) + lines + .iter() + .map(|line| { + measure_line_width(line, self.style.font_size, self.style.letter_spacing) + }) .fold(0.0, f32::max) .min(available_width) }; - + let height = lines.len() as f32 * line_height; - + let size = Size::new(width, height); self.measured_size.set(size); self.cached_lines.set(lines); - + size } @@ -544,7 +547,7 @@ impl Text { measured.height.min(available_size.height), ); } - + self.measure_text(available_size.width) } @@ -565,16 +568,16 @@ impl Text { // Calculate character position (simplified) let relative_x = point.x - bounds.x; let relative_y = point.y - bounds.y; - + let char_width = self.style.font_size * 0.6; let line_height = self.style.font_size * self.style.line_height; - + let line = (relative_y / line_height) as usize; let char_in_line = (relative_x / char_width) as usize; - + // Simple character position calculation let position = char_in_line.min(self.content.get().len()); - + self.set_selection(Some(position), Some(position)); return true; } @@ -593,8 +596,9 @@ impl Text { let relative_x = point.x - bounds.x; let char_width = self.style.font_size * 0.6; let position = (relative_x / char_width) as usize; - - self.selection_end.set(Some(position.min(self.content.get().len()))); + + self.selection_end + .set(Some(position.min(self.content.get().len()))); return true; } } @@ -608,23 +612,26 @@ impl Text { } let bounds = self.bounds.get(); - + // Apply clipping if needed - let should_clip = matches!(self.style.text_overflow, TextOverflow::Clip | TextOverflow::Scroll); + let should_clip = matches!( + self.style.text_overflow, + TextOverflow::Clip | TextOverflow::Scroll + ); if should_clip { batch.push_clip(bounds); } - + // Render selection background if any if let Some((start, end)) = self.get_selection() { if start != end { let selection_color = Color::rgba(0.0, 0.4, 0.8, 0.3); - + // Simple selection rendering (would need proper text metrics) let char_width = self.style.font_size * 0.6; let selection_x = bounds.x + start as f32 * char_width; let selection_width = (end - start) as f32 * char_width; - + let (vertices, indices) = VertexBuilder::rectangle( selection_x, bounds.y, @@ -635,13 +642,14 @@ impl Text { batch.add_vertices(&vertices, &indices); } } - + let lines = self.cached_lines.get(); let line_height = self.style.font_size * self.style.line_height; for (i, line) in lines.iter().enumerate() { - let line_width = measure_line_width(line, self.style.font_size, self.style.letter_spacing); - + let line_width = + measure_line_width(line, self.style.font_size, self.style.letter_spacing); + // Calculate text position based on alignment let text_x = match self.style.text_align { TextAlign::Left => bounds.x, @@ -649,14 +657,23 @@ impl Text { TextAlign::Right => bounds.x + bounds.width - line_width, TextAlign::Justify => bounds.x, // Simplified }; - + let text_y = match self.style.vertical_align { VerticalAlign::Top => bounds.y + (i as f32 * line_height), - VerticalAlign::Middle => bounds.y + (bounds.height - (lines.len() as f32 * line_height)) / 2.0 + (i as f32 * line_height), - VerticalAlign::Bottom => bounds.y + bounds.height - (lines.len() as f32 * line_height) + (i as f32 * line_height), - VerticalAlign::Baseline => bounds.y + (i as f32 * line_height) + self.style.font_size * 0.8, + VerticalAlign::Middle => { + bounds.y + + (bounds.height - (lines.len() as f32 * line_height)) / 2.0 + + (i as f32 * line_height) + } + VerticalAlign::Bottom => { + bounds.y + bounds.height - (lines.len() as f32 * line_height) + + (i as f32 * line_height) + } + VerticalAlign::Baseline => { + bounds.y + (i as f32 * line_height) + self.style.font_size * 0.8 + } }; - + // Render line batch.add_text( line.clone(), @@ -674,7 +691,7 @@ impl Text { TextDecoration::LineThrough => text_y + self.style.font_size / 2.0, TextDecoration::None => text_y, }; - + let (vertices, indices) = VertexBuilder::line( text_x, decoration_y, @@ -686,11 +703,11 @@ impl Text { batch.add_vertices(&vertices, &indices); } } - + if should_clip { batch.pop_clip(); } - + // TODO: Re-implement TextSpan support for multi-line text // This requires mapping lines back to original string indices /* @@ -699,7 +716,7 @@ impl Text { if let Some(span_style) = &span.style { let span_text = &span.text[span.start..span.end.min(span.text.len())]; let span_x = text_x + span.start as f32 * self.style.font_size * 0.6; - + batch.add_text( span_text.to_string(), (span_x, text_y), @@ -858,7 +875,7 @@ mod tests { let heading = Text::new("Heading").heading(1); let body = Text::new("Body").body(); let caption = Text::new("Caption").caption(); - + assert!(heading.style.font_size > body.style.font_size); assert!(body.style.font_size > caption.style.font_size); } @@ -866,13 +883,13 @@ mod tests { #[test] fn test_text_selection() { let text = Text::new("Test selection").selectable(true); - + assert!(text.is_selectable()); assert_eq!(text.get_selection(), None); - + text.set_selection(Some(0), Some(4)); assert_eq!(text.get_selection(), Some((0, 4))); - + text.clear_selection(); assert_eq!(text.get_selection(), None); } @@ -884,7 +901,7 @@ mod tests { .color(Color::rgba(1.0, 0.0, 0.0, 1.0)) .selectable(true) .build(); - + assert_eq!(text.content(), "Builder Test"); assert!(text.is_selectable()); assert_eq!(text.style.color, Color::rgba(1.0, 0.0, 0.0, 1.0)); @@ -895,7 +912,7 @@ mod tests { let text = Text::new("Test measurement"); let available = Size::new(200.0, 100.0); let size = text.calculate_size(available); - + assert!(size.width > 0.0); assert!(size.height > 0.0); assert!(size.width <= available.width); @@ -919,16 +936,16 @@ impl Widget for Text { layout.position.x, layout.position.y, layout.size.width, - layout.size.height + layout.size.height, )); - + // Ensure text is measured/wrapped for these bounds // Note: layout() should have been called before render(), but we need to ensure // cached_lines are up to date for the current width. // Since render() is const, we rely on layout() having populated cached_lines. // If layout wasn't called or width changed, we might render stale lines. // Ideally measure_text should be called here if needed, but we can't mutate. - + self.render(batch); } diff --git a/crates/strato-widgets/src/theme.rs b/crates/strato-widgets/src/theme.rs index f98805e..37f0a12 100644 --- a/crates/strato-widgets/src/theme.rs +++ b/crates/strato-widgets/src/theme.rs @@ -133,7 +133,7 @@ impl ColorPalette { /// High contrast color palette pub fn high_contrast() -> Self { Self { - primary: Color::rgb(0.0, 1.0, 1.0), // Cyan + primary: Color::rgb(0.0, 1.0, 1.0), // Cyan secondary: Color::rgb(1.0, 1.0, 0.0), // Yellow success: Color::rgb(0.0, 1.0, 0.0), warning: Color::rgb(1.0, 0.5, 0.0), @@ -180,32 +180,84 @@ impl Default for Typography { // Use platform-specific default fonts with proper fallbacks #[cfg(target_os = "windows")] let font_family = "Segoe UI, Tahoma, Arial, sans-serif"; - + #[cfg(target_os = "macos")] let font_family = "SF Pro Display, Helvetica Neue, Arial, sans-serif"; - + #[cfg(target_os = "linux")] let font_family = "Ubuntu, DejaVu Sans, Liberation Sans, Arial, sans-serif"; - + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let font_family = "Arial, sans-serif"; - + Self { font_family: font_family.to_string(), font_family_mono: "Consolas, Monaco, monospace".to_string(), - h1: TextStyle { size: 96.0, weight: 300, letter_spacing: -1.5 }, - h2: TextStyle { size: 60.0, weight: 300, letter_spacing: -0.5 }, - h3: TextStyle { size: 48.0, weight: 400, letter_spacing: 0.0 }, - h4: TextStyle { size: 34.0, weight: 400, letter_spacing: 0.25 }, - h5: TextStyle { size: 24.0, weight: 400, letter_spacing: 0.0 }, - h6: TextStyle { size: 20.0, weight: 500, letter_spacing: 0.15 }, - body1: TextStyle { size: 16.0, weight: 400, letter_spacing: 0.5 }, - body2: TextStyle { size: 14.0, weight: 400, letter_spacing: 0.25 }, - subtitle1: TextStyle { size: 16.0, weight: 400, letter_spacing: 0.15 }, - subtitle2: TextStyle { size: 14.0, weight: 500, letter_spacing: 0.1 }, - button: TextStyle { size: 14.0, weight: 500, letter_spacing: 1.25 }, - caption: TextStyle { size: 12.0, weight: 400, letter_spacing: 0.4 }, - overline: TextStyle { size: 10.0, weight: 400, letter_spacing: 1.5 }, + h1: TextStyle { + size: 96.0, + weight: 300, + letter_spacing: -1.5, + }, + h2: TextStyle { + size: 60.0, + weight: 300, + letter_spacing: -0.5, + }, + h3: TextStyle { + size: 48.0, + weight: 400, + letter_spacing: 0.0, + }, + h4: TextStyle { + size: 34.0, + weight: 400, + letter_spacing: 0.25, + }, + h5: TextStyle { + size: 24.0, + weight: 400, + letter_spacing: 0.0, + }, + h6: TextStyle { + size: 20.0, + weight: 500, + letter_spacing: 0.15, + }, + body1: TextStyle { + size: 16.0, + weight: 400, + letter_spacing: 0.5, + }, + body2: TextStyle { + size: 14.0, + weight: 400, + letter_spacing: 0.25, + }, + subtitle1: TextStyle { + size: 16.0, + weight: 400, + letter_spacing: 0.15, + }, + subtitle2: TextStyle { + size: 14.0, + weight: 500, + letter_spacing: 0.1, + }, + button: TextStyle { + size: 14.0, + weight: 500, + letter_spacing: 1.25, + }, + caption: TextStyle { + size: 12.0, + weight: 400, + letter_spacing: 0.4, + }, + overline: TextStyle { + size: 10.0, + weight: 400, + letter_spacing: 1.5, + }, } } } @@ -320,7 +372,10 @@ impl ComponentThemes { pub fn light() -> Self { let mut button = HashMap::new(); button.insert("primary".to_string(), crate::button::ButtonStyle::primary()); - button.insert("secondary".to_string(), crate::button::ButtonStyle::secondary()); + button.insert( + "secondary".to_string(), + crate::button::ButtonStyle::secondary(), + ); button.insert("text".to_string(), crate::button::ButtonStyle::ghost()); let mut input = HashMap::new(); @@ -355,7 +410,7 @@ impl ThemeProvider { let mut themes = HashMap::new(); themes.insert("light".to_string(), Theme::light()); themes.insert("dark".to_string(), Theme::dark()); - + Self { current: theme, themes, diff --git a/crates/strato-widgets/src/top_bar.rs b/crates/strato-widgets/src/top_bar.rs index a19c7c4..8f54dc3 100644 --- a/crates/strato-widgets/src/top_bar.rs +++ b/crates/strato-widgets/src/top_bar.rs @@ -1,23 +1,23 @@ -use strato_core::{ - types::{Color, Point}, - layout::{Constraints, Layout, Size}, - event::{Event, EventResult}, -}; use crate::{ - Widget, container::Container, - layout::{Row, MainAxisAlignment, CrossAxisAlignment}, - text::{Text, FontWeight}, - widget::{WidgetId, WidgetContext, generate_id}, + layout::{CrossAxisAlignment, MainAxisAlignment, Row}, + text::{FontWeight, Text}, + widget::{generate_id, WidgetContext, WidgetId}, + Widget, }; use std::any::Any; +use strato_core::{ + event::{Event, EventResult}, + layout::{Constraints, Layout, Size}, + types::{Color, Point}, +}; /// A standardized top bar / header widget #[derive(Debug)] pub struct TopBar { id: WidgetId, inner: Option>, - + // Props pub title: String, pub leading: Option>, @@ -62,14 +62,16 @@ impl TopBar { self.inner = None; self } - + fn ensure_inner(&mut self) { - if self.inner.is_some() { return; } - + if self.inner.is_some() { + return; + } + // Build the inner widget tree let title_widget = Text::new(&self.title) .size(16.0) - .font_weight(FontWeight::SemiBold) + .font_weight(FontWeight::SemiBold) .color(Color::WHITE); let mut row = Row::new() @@ -77,7 +79,6 @@ impl TopBar { .cross_axis_alignment(CrossAxisAlignment::Center) .spacing(16.0); - if let Some(leading) = &self.leading { row = row.child(leading.clone_widget()); } @@ -87,7 +88,7 @@ impl TopBar { if let Some(trailing) = &self.trailing { row = row.child(trailing.clone_widget()); } - + let container = Container::new() .width(f32::MAX) // Full width .height(self.height) @@ -100,30 +101,32 @@ impl TopBar { } impl Widget for TopBar { - fn id(&self) -> WidgetId { self.id } - + fn id(&self) -> WidgetId { + self.id + } + fn layout(&mut self, constraints: Constraints) -> Size { self.ensure_inner(); self.inner.as_mut().unwrap().layout(constraints) } - + fn render(&self, batch: &mut strato_renderer::batch::RenderBatch, layout: Layout) { if let Some(inner) = &self.inner { inner.render(batch, layout); } } - + fn handle_event(&mut self, event: &Event) -> EventResult { self.ensure_inner(); self.inner.as_mut().unwrap().handle_event(event) } - + fn update(&mut self, ctx: &WidgetContext) { if let Some(inner) = &mut self.inner { inner.update(ctx); } } - + fn children(&self) -> Vec<&(dyn Widget + '_)> { if let Some(inner) = &self.inner { vec![inner.as_ref()] @@ -131,7 +134,7 @@ impl Widget for TopBar { vec![] } } - + fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> { if let Some(inner) = &mut self.inner { vec![inner.as_mut()] @@ -139,7 +142,7 @@ impl Widget for TopBar { vec![] } } - + fn hit_test(&self, point: Point, layout: Layout) -> bool { if let Some(inner) = &self.inner { inner.hit_test(point, layout) @@ -147,14 +150,18 @@ impl Widget for TopBar { false } } - - fn as_any(&self) -> &dyn Any { self } - fn as_any_mut(&mut self) -> &mut dyn Any { self } - + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn clone_widget(&self) -> Box { Box::new(Self { id: generate_id(), // New ID - inner: None, // Reset inner to force rebuild/fresh state + inner: None, // Reset inner to force rebuild/fresh state title: self.title.clone(), leading: self.leading.as_ref().map(|w| w.clone_widget()), trailing: self.trailing.as_ref().map(|w| w.clone_widget()), diff --git a/crates/strato-widgets/src/widget.rs b/crates/strato-widgets/src/widget.rs index c124e03..912ec3d 100644 --- a/crates/strato-widgets/src/widget.rs +++ b/crates/strato-widgets/src/widget.rs @@ -1,13 +1,13 @@ //! Base widget trait and common functionality +use std::any::Any; +use std::fmt::Debug; use strato_core::{ event::{Event, EventResult}, layout::{Constraints, Layout, Size}, - types::{Point}, // Removed unused Color and Rect imports + types::Point, // Removed unused Color and Rect imports }; use strato_renderer::batch::RenderBatch; -use std::any::Any; -use std::fmt::Debug; /// Unique widget identifier pub type WidgetId = u64; diff --git a/crates/strato-widgets/src/wrap.rs b/crates/strato-widgets/src/wrap.rs index c3051b5..9fd0258 100644 --- a/crates/strato-widgets/src/wrap.rs +++ b/crates/strato-widgets/src/wrap.rs @@ -1,14 +1,14 @@ //! Wrap widget for flow layout -use crate::widget::{Widget, WidgetId, generate_id}; +use crate::widget::{generate_id, Widget, WidgetId}; +use std::any::Any; use strato_core::{ event::{Event, EventResult}, layout::{ - Constraints, Layout, Size, FlexItem, FlexContainer, FlexDirection, - FlexWrap, JustifyContent, AlignItems, AlignContent, Gap, + AlignContent, AlignItems, Constraints, FlexContainer, FlexDirection, FlexItem, FlexWrap, + Gap, JustifyContent, Layout, Size, }, }; use strato_renderer::batch::RenderBatch; -use std::any::Any; /// Alignment for wrap layout #[derive(Debug, Clone, Copy, PartialEq)] @@ -116,7 +116,7 @@ impl Widget for Wrap { fn layout(&mut self, constraints: Constraints) -> Size { let engine = strato_core::layout::LayoutEngine::new(); - + // Relax constraints for children measurement // Wrap children can be any size, they force a wrap if they exceed width let child_constraints = Constraints { @@ -125,21 +125,21 @@ impl Widget for Wrap { min_height: 0.0, max_height: constraints.max_height, }; - + // Calculate child sizes let mut child_data = Vec::new(); let mut sizes = Vec::with_capacity(self.children.len()); for child in &mut self.children { let child_size = child.layout(child_constraints); sizes.push(child_size); - + // Wrap treats all items as non-flex by default in terms of growing to fill line // But we can support flex basis if needed. For now, we assume simple flow. let flex_item = FlexItem::default(); child_data.push((flex_item, child_size)); } self.cached_child_sizes = sizes; - + // Map widget props to core layout props let justify_content = match self.alignment { WrapAlignment::Start => JustifyContent::FlexStart, @@ -172,35 +172,39 @@ impl Widget for Wrap { align_items, align_content, gap: Gap { - row: self.run_spacing, // Gap between runs (lines) - column: self.spacing, // Gap between items + row: self.run_spacing, // Gap between runs (lines) + column: self.spacing, // Gap between items }, ..Default::default() }; let layouts = engine.calculate_flex_layout(&container, &child_data, constraints); - + // Calculate total size based on returned layouts let mut max_x = 0.0f32; let mut max_y = 0.0f32; for l in layouts { - max_x = max_x.max(l.position.x + l.size.width); - max_y = max_y.max(l.position.y + l.size.height); + max_x = max_x.max(l.position.x + l.size.width); + max_y = max_y.max(l.position.y + l.size.height); } - + Size::new(max_x, max_y) } fn render(&self, batch: &mut RenderBatch, layout: Layout) { let engine = strato_core::layout::LayoutEngine::new(); - + // Reconstruct child data from cache let mut child_data = Vec::new(); for (i, _) in self.children.iter().enumerate() { - let child_size = self.cached_child_sizes.get(i).copied().unwrap_or(Size::zero()); + let child_size = self + .cached_child_sizes + .get(i) + .copied() + .unwrap_or(Size::zero()); child_data.push((FlexItem::default(), child_size)); } - + // Map props again (should factor this out if it gets complex) let justify_content = match self.alignment { WrapAlignment::Start => JustifyContent::FlexStart, @@ -241,17 +245,15 @@ impl Widget for Wrap { // Recalculate layout inside bounds let layouts = engine.calculate_flex_layout( - &container, - &child_data, - Constraints::tight(layout.size.width, layout.size.height) // Use actual assigned size + &container, + &child_data, + Constraints::tight(layout.size.width, layout.size.height), // Use actual assigned size ); - + // Render children for (child, child_layout) in self.children.iter().zip(layouts.iter()) { - let absolute_layout = Layout::new( - layout.position + child_layout.position, - child_layout.size, - ); + let absolute_layout = + Layout::new(layout.position + child_layout.position, child_layout.size); child.render(batch, absolute_layout); } } diff --git a/crates/strato-widgets/tests/control_snapshots.rs b/crates/strato-widgets/tests/control_snapshots.rs new file mode 100644 index 0000000..7b9d31b --- /dev/null +++ b/crates/strato-widgets/tests/control_snapshots.rs @@ -0,0 +1,124 @@ +use glam::Vec2; +use std::collections::BTreeMap; +use strato_core::layout::{Constraints, Layout}; +use strato_renderer::{batch::DrawCommand, batch::RenderBatch}; +use strato_widgets::button::ButtonState; +use strato_widgets::{Button, Slider, Theme, Widget}; + +fn summarize_batch(batch: &RenderBatch) -> Vec { + batch + .commands + .iter() + .map(|cmd| match cmd { + DrawCommand::Rect { rect, color, .. } => format!( + "rect {:.1},{:.1} {:.1}x{:.1} rgba({:.2},{:.2},{:.2},{:.2})", + rect.x, rect.y, rect.width, rect.height, color.r, color.g, color.b, color.a + ), + DrawCommand::Text { + text, + position, + color, + font_size, + .. + } => format!( + "text '{}' @({:.1},{:.1}) size {:.1} rgba({:.2},{:.2},{:.2},{:.2})", + text, position.0, position.1, font_size, color.r, color.g, color.b, color.a + ), + DrawCommand::Circle { + center, + radius, + color, + .. + } => format!( + "circle ({:.1},{:.1}) r{:.1} rgba({:.2},{:.2},{:.2},{:.2})", + center.0, center.1, radius, color.r, color.g, color.b, color.a + ), + other => format!("{:?}", other), + }) + .collect() +} + +fn render_button_with_state(state: ButtonState) -> Vec { + let theme = Theme::default(); + let mut button = Button::new(format!("{:?}", state)).size(140.0, 44.0); + button.set_state(state); + + let mut batch = RenderBatch::new(); + let size =