StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Container widget for layout and styling |
| 2 | |
| 3 | use crate::widget::{generate_id, Widget, WidgetId}; |
| 4 | use std::any::Any; |
| 5 | use strato_core::{ |
| 6 | event::{Event, EventResult}, |
| 7 | layout::{Constraints, EdgeInsets, Layout, Size}, |
| 8 | state::Signal, |
| 9 | types::{BorderRadius, Color, Point, Rect, Shadow}, |
| 10 | Transform, |
| 11 | }; |
| 12 | use strato_renderer::batch::RenderBatch; |
| 13 | |
| 14 | /// Container widget for grouping and styling child widgets |
| 15 | pub struct Container { |
| 16 | id: WidgetId, |
| 17 | child: Option<Box<dyn Widget>>, |
| 18 | style: ContainerStyle, |
| 19 | constraints: Option<Constraints>, |
| 20 | on_click: Option<Box<dyn Fn() + Send + Sync>>, |
| 21 | on_hover: Option<Box<dyn Fn(bool) + Send + Sync>>, |
| 22 | state: Signal<ContainerState>, |
| 23 | bounds: Signal<Rect>, |
| 24 | } |
| 25 | |
| 26 | impl std::fmt::Debug for Container { |
| 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 28 | f.debug_struct("Container") |
| 29 | .field("id", &self.id) |
| 30 | .field("child", &self.child) |
| 31 | .field("style", &self.style) |
| 32 | .field("constraints", &self.constraints) |
| 33 | .field("on_click", &self.on_click.as_ref().map(|_| "Fn()")) |
| 34 | .field("on_hover", &self.on_hover.as_ref().map(|_| "Fn(bool)")) |
| 35 | .field("state", &self.state) |
| 36 | .field("bounds", &self.bounds) |
| 37 | .finish() |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | #[derive(Debug, Clone, Copy, PartialEq, Default)] |
| 42 | struct ContainerState { |
| 43 | hovered: bool, |
| 44 | pressed: bool, |
| 45 | } |
| 46 | |
| 47 | impl Container { |
| 48 | /// Create a new container |
| 49 | pub fn new() -> Self { |
| 50 | Self { |
| 51 | id: generate_id(), |
| 52 | child: None, |
| 53 | style: ContainerStyle::default(), |
| 54 | constraints: None, |
| 55 | on_click: None, |
| 56 | on_hover: None, |
| 57 | state: Signal::new(ContainerState::default()), |
| 58 | bounds: Signal::new(Rect::default()), |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// Set the child widget |
| 63 | pub fn child(mut self, child: impl Widget + 'static) -> Self { |
| 64 | self.child = Some(Box::new(child)); |
| 65 | self |
| 66 | } |
| 67 | |
| 68 | /// Set padding |
| 69 | pub fn padding(mut self, padding: f32) -> Self { |
| 70 | self.style.padding = EdgeInsets::all(padding); |
| 71 | self |
| 72 | } |
| 73 | |
| 74 | /// Set padding with individual values |
| 75 | pub fn padding_values(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self { |
| 76 | self.style.padding = EdgeInsets { |
| 77 | top, |
| 78 | right, |
| 79 | bottom, |
| 80 | left, |
| 81 | }; |
| 82 | self |
| 83 | } |
| 84 | |
| 85 | /// Set margin |
| 86 | pub fn margin(mut self, margin: f32) -> Self { |
| 87 | self.style.margin = EdgeInsets::all(margin); |
| 88 | self |
| 89 | } |
| 90 | |
| 91 | /// Set background color |
| 92 | pub fn background(mut self, color: Color) -> Self { |
| 93 | self.style.background_color = color; |
| 94 | self |
| 95 | } |
| 96 | |
| 97 | /// Set border |
| 98 | pub fn border(mut self, width: f32, color: Color) -> Self { |
| 99 | self.style.border_width = width; |
| 100 | self.style.border_color = color; |
| 101 | self |
| 102 | } |
| 103 | |
| 104 | /// Set border radius |
| 105 | pub fn border_radius(mut self, radius: f32) -> Self { |
| 106 | self.style.border_radius = BorderRadius::all(radius); |
| 107 | self |
| 108 | } |
| 109 | |
| 110 | /// Set shadow |
| 111 | pub fn shadow(mut self, shadow: Shadow) -> Self { |
| 112 | self.style.shadow = Some(shadow); |
| 113 | self |
| 114 | } |
| 115 | |
| 116 | /// Set width |
| 117 | pub fn width(mut self, width: f32) -> Self { |
| 118 | self.style.width = Some(width); |
| 119 | self |
| 120 | } |
| 121 | |
| 122 | /// Set height |
| 123 | pub fn height(mut self, height: f32) -> Self { |
| 124 | self.style.height = Some(height); |
| 125 | self |
| 126 | } |
| 127 | |
| 128 | /// Set both width and height |
| 129 | pub fn size(mut self, width: f32, height: f32) -> Self { |
| 130 | self.style.width = Some(width); |
| 131 | self.style.height = Some(height); |
| 132 | self |
| 133 | } |
| 134 | |
| 135 | /// Set style |
| 136 | pub fn style(mut self, style: ContainerStyle) -> Self { |
| 137 | self.style = style; |
| 138 | self |
| 139 | } |
| 140 | |
| 141 | /// Set constraints |
| 142 | pub fn constraints(mut self, constraints: Constraints) -> Self { |
| 143 | self.constraints = Some(constraints); |
| 144 | self |
| 145 | } |
| 146 | |
| 147 | /// Set click handler |
| 148 | pub fn on_click<F>(mut self, handler: F) -> Self |
| 149 | where |
| 150 | F: Fn() + Send + Sync + 'static, |
| 151 | { |
| 152 | self.on_click = Some(Box::new(handler)); |
| 153 | self |
| 154 | } |
| 155 | |
| 156 | /// Set hover handler |
| 157 | pub fn on_hover<F>(mut self, handler: F) -> Self |
| 158 | where |
| 159 | F: Fn(bool) + Send + Sync + 'static, |
| 160 | { |
| 161 | self.on_hover = Some(Box::new(handler)); |
| 162 | self |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | impl Widget for Container { |
| 167 | fn id(&self) -> WidgetId { |
| 168 | self.id |
| 169 | } |
| 170 | |
| 171 | fn layout(&mut self, constraints: Constraints) -> Size { |
| 172 | let constraints = self.constraints.unwrap_or(constraints); |
| 173 | |
| 174 | // Apply margin to constraints |
| 175 | let margin = self.style.margin; |
| 176 | let inner_constraints = Constraints { |
| 177 | min_width: (constraints.min_width - margin.horizontal()).max(0.0), |
| 178 | max_width: (constraints.max_width - margin.horizontal()).max(0.0), |
| 179 | min_height: (constraints.min_height - margin.vertical()).max(0.0), |
| 180 | max_height: (constraints.max_height - margin.vertical()).max(0.0), |
| 181 | }; |
| 182 | |
| 183 | // Apply padding to child constraints |
| 184 | let padding = self.style.padding; |
| 185 | let child_constraints = Constraints { |
| 186 | min_width: (inner_constraints.min_width - padding.horizontal()).max(0.0), |
| 187 | max_width: (inner_constraints.max_width - padding.horizontal()).max(0.0), |
| 188 | min_height: (inner_constraints.min_height - padding.vertical()).max(0.0), |
| 189 | max_height: (inner_constraints.max_height - padding.vertical()).max(0.0), |
| 190 | }; |
| 191 | |
| 192 | // Calculate child size |
| 193 | let child_size = if let Some(child) = &mut self.child { |
| 194 | child.layout(child_constraints) |
| 195 | } else { |
| 196 | Size::zero() |
| 197 | }; |
| 198 | |
| 199 | // Calculate container size |
| 200 | let mut width = child_size.width + padding.horizontal(); |
| 201 | let mut height = child_size.height + padding.vertical(); |
| 202 | |
| 203 | // Apply fixed dimensions if specified |
| 204 | if let Some(fixed_width) = self.style.width { |
| 205 | width = fixed_width; |
| 206 | } |
| 207 | if let Some(fixed_height) = self.style.height { |
| 208 | height = fixed_height; |
| 209 | } |
| 210 | |
| 211 | // Add margin |
| 212 | width += margin.horizontal(); |
| 213 | height += margin.vertical(); |
| 214 | |
| 215 | // Constrain to limits |
| 216 | Size::new( |
| 217 | width.clamp(constraints.min_width, constraints.max_width), |
| 218 | height.clamp(constraints.min_height, constraints.max_height), |
| 219 | ) |
| 220 | } |
| 221 | |
| 222 | fn render(&self, batch: &mut RenderBatch, layout: Layout) { |
| 223 | let bounds = Rect::new( |
| 224 | layout.position.x, |
| 225 | layout.position.y, |
| 226 | layout.size.width, |
| 227 | layout.size.height, |
| 228 | ); |
| 229 | self.bounds.set(bounds); |
| 230 | |
| 231 | let margin = self.style.margin; |
| 232 | let padding = self.style.padding; |
| 233 | |
| 234 | // Calculate content rect (excluding margin) |
| 235 | let content_rect = Rect::new( |
| 236 | layout.position.x + margin.left, |
| 237 | layout.position.y + margin.top, |
| 238 | layout.size.width - margin.horizontal(), |
| 239 | layout.size.height - margin.vertical(), |
| 240 | ); |
| 241 | |
| 242 | // Draw shadow if present |
| 243 | if let Some(shadow) = &self.style.shadow { |
| 244 | let _shadow_rect = content_rect.expand(shadow.spread_radius); |
| 245 | // TODO: Implement proper shadow rendering |
| 246 | } |
| 247 | |
| 248 | // Draw background with state feedback |
| 249 | let mut background_color = self.style.background_color; |
| 250 | let state = self.state.get(); |
| 251 | |
| 252 | if state.pressed { |
| 253 | background_color = background_color.darken(0.2); // Visual feedback for press |
| 254 | } else if state.hovered { |
| 255 | background_color = background_color.lighten(0.1); // Visual feedback for hover |
| 256 | } |
| 257 | |
| 258 | if background_color.a > 0.0 { |
| 259 | batch.add_rect(content_rect, background_color, Transform::identity()); |
| 260 | } |
| 261 | |
| 262 | // Draw border |
| 263 | if self.style.border_width > 0.0 { |
| 264 | // TODO: Implement proper border rendering |
| 265 | } |
| 266 | |
| 267 | // Render child |
| 268 | if let Some(child) = &self.child { |
| 269 | let child_layout = Layout::new( |
| 270 | glam::Vec2::new(content_rect.x + padding.left, content_rect.y + padding.top), |
| 271 | Size::new( |
| 272 | content_rect.width - padding.horizontal(), |
| 273 | content_rect.height - padding.vertical(), |
| 274 | ), |
| 275 | ); |
| 276 | child.render(batch, child_layout); |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | fn handle_event(&mut self, event: &Event) -> EventResult { |
| 281 | // Handle interactions if callbacks are present |
| 282 | if self.on_click.is_some() || self.on_hover.is_some() { |
| 283 | match event { |
| 284 | Event::MouseMove(mouse_event) => { |
| 285 | let bounds = self.bounds.get(); |
| 286 | let point = Point::new(mouse_event.position.x, mouse_event.position.y); |
| 287 | let is_hovered = bounds.contains(point); |
| 288 | let mut state = self.state.get(); |
| 289 | |
| 290 | if is_hovered != state.hovered { |
| 291 | state.hovered = is_hovered; |
| 292 | self.state.set(state); |
| 293 | if let Some(handler) = &self.on_hover { |
| 294 | handler(is_hovered); |
| 295 | } |
| 296 | } |
| 297 | if is_hovered { |
| 298 | // Don't necessarily block children, but track state |
| 299 | } |
| 300 | } |
| 301 | Event::MouseDown(mouse_event) => { |
| 302 | let bounds = self.bounds.get(); |
| 303 | let point = Point::new(mouse_event.position.x, mouse_event.position.y); |
| 304 | if bounds.contains(point) { |
| 305 | let mut state = self.state.get(); |
| 306 | state.pressed = true; |
| 307 | self.state.set(state); |
| 308 | } |
| 309 | } |
| 310 | Event::MouseUp(mouse_event) => { |
| 311 | let bounds = self.bounds.get(); |
| 312 | let point = Point::new(mouse_event.position.x, mouse_event.position.y); |
| 313 | let mut state = self.state.get(); |
| 314 | |
| 315 | if state.pressed { |
| 316 | state.pressed = false; |
| 317 | self.state.set(state); |
| 318 | if bounds.contains(point) { |
| 319 | if let Some(handler) = &self.on_click { |
| 320 | handler(); |
| 321 | // If we clicked, we probably handled it. But child might have handled it? |
| 322 | // If child handled it, its result would be Handled. |
| 323 | } |
| 324 | } |
| 325 | } |
| 326 | } |
| 327 | _ => {} |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | // Delegate to child FIRST to allow inner interactive elements to work |
| 332 | if let Some(child) = &mut self.child { |
| 333 | let child_result = child.handle_event(event); |
| 334 | if child_result == EventResult::Handled { |
| 335 | return EventResult::Handled; |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | // If child didn't handle it, AND we have interactions, check if we should handle it |
| 340 | if self.on_click.is_some() { |
| 341 | match event { |
| 342 | Event::MouseDown(e) => { |
| 343 | let bounds = self.bounds.get(); |
| 344 | if bounds.contains(Point::new(e.position.x, e.position.y)) { |
| 345 | return EventResult::Handled; |
| 346 | } |
| 347 | } |
| 348 | Event::MouseUp(e) => { |
| 349 | let bounds = self.bounds.get(); |
| 350 | if bounds.contains(Point::new(e.position.x, e.position.y)) { |
| 351 | // And was pressed logic... |
| 352 | return EventResult::Handled; |
| 353 | } |
| 354 | } |
| 355 | _ => {} |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | EventResult::Ignored |
| 360 | } |
| 361 | |
| 362 | fn children(&self) -> Vec<&(dyn Widget + '_)> { |
| 363 | if let Some(child) = &self.child { |
| 364 | vec![child.as_ref()] |
| 365 | } else { |
| 366 | vec![] |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> { |
| 371 | if let Some(child) = &mut self.child { |
| 372 | vec![child.as_mut()] |
| 373 | } else { |
| 374 | vec![] |
| 375 | } |
| 376 | } |
| 377 | |
| 378 | fn as_any(&self) -> &dyn Any { |
| 379 | self |
| 380 | } |
| 381 | |
| 382 | fn as_any_mut(&mut self) -> &mut dyn Any { |
| 383 | self |
| 384 | } |
| 385 | |
| 386 | fn clone_widget(&self) -> Box<dyn Widget> { |
| 387 | Box::new(Container { |
| 388 | id: generate_id(), |
| 389 | child: self.child.as_ref().map(|c| c.clone_widget()), |
| 390 | style: self.style.clone(), |
| 391 | constraints: self.constraints, |
| 392 | on_click: None, |
| 393 | on_hover: None, |
| 394 | state: Signal::new(self.state.get()), |
| 395 | bounds: Signal::new(self.bounds.get()), |
| 396 | }) |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | impl Default for Container { |
| 401 | fn default() -> Self { |
| 402 | Self::new() |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | /// Container style configuration |
| 407 | #[derive(Debug, Clone)] |
| 408 | pub struct ContainerStyle { |
| 409 | pub background_color: Color, |
| 410 | pub border_color: Color, |
| 411 | pub border_width: f32, |
| 412 | pub border_radius: BorderRadius, |
| 413 | pub padding: EdgeInsets, |
| 414 | pub margin: EdgeInsets, |
| 415 | pub shadow: Option<Shadow>, |
| 416 | pub width: Option<f32>, |
| 417 | pub height: Option<f32>, |
| 418 | } |
| 419 | |
| 420 | impl Default for ContainerStyle { |
| 421 | fn default() -> Self { |
| 422 | Self { |
| 423 | background_color: Color::TRANSPARENT, |
| 424 | border_color: Color::TRANSPARENT, |
| 425 | border_width: 0.0, |
| 426 | border_radius: BorderRadius::all(0.0), |
| 427 | padding: EdgeInsets::all(0.0), |
| 428 | margin: EdgeInsets::all(0.0), |
| 429 | shadow: None, |
| 430 | width: None, |
| 431 | height: None, |
| 432 | } |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | impl ContainerStyle { |
| 437 | /// Card style with shadow |
| 438 | pub fn card() -> Self { |
| 439 | Self { |
| 440 | background_color: Color::WHITE, |
| 441 | border_color: Color::rgba(0.0, 0.0, 0.0, 0.1), |
| 442 | border_width: 1.0, |
| 443 | border_radius: BorderRadius::all(8.0), |
| 444 | padding: EdgeInsets::all(16.0), |
| 445 | margin: EdgeInsets::all(8.0), |
| 446 | shadow: Some(Shadow::drop(4.0)), |
| 447 | width: None, |
| 448 | height: None, |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | /// Panel style |
| 453 | pub fn panel() -> Self { |
| 454 | Self { |
| 455 | background_color: Color::rgba(0.95, 0.95, 0.95, 1.0), |
| 456 | border_color: Color::rgba(0.0, 0.0, 0.0, 0.2), |
| 457 | border_width: 1.0, |
| 458 | border_radius: BorderRadius::all(4.0), |
| 459 | padding: EdgeInsets::all(12.0), |
| 460 | margin: EdgeInsets::all(0.0), |
| 461 | shadow: None, |
| 462 | width: None, |
| 463 | height: None, |
| 464 | } |
| 465 | } |
| 466 | } |
| 467 |