Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/container.rs
StratoSDK / crates / strato-widgets / src / container.rs
1//! Container widget for layout and styling
2 
3use crate::widget::{generate_id, Widget, WidgetId};
4use std::any::Any;
5use 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};
12use strato_renderer::batch::RenderBatch;
13 
14/// Container widget for grouping and styling child widgets
15pub 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 
26impl 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)]
42struct ContainerState {
43 hovered: bool,
44 pressed: bool,
45}
46 
47impl 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 
166impl 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 
400impl Default for Container {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405 
406/// Container style configuration
407#[derive(Debug, Clone)]
408pub 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 
420impl 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 
436impl 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