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-ui-renderer/examples/table-sample/root_view.rs
1use crate::CaptureConfig;
2use image::ImageEncoder;
3use std::sync::{Arc, Mutex};
4use strato_ui::color::ColorU;
5use strato_ui::elements::{
6 ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container, Empty, Fill, Flex,
7 MainAxisSize, ParentElement, RowBackground, ScrollStateHandle, Scrollable, ScrollableElement,
8 ScrollbarWidth, SelectableArea, SelectionHandle, Table, TableColumnWidth, TableConfig,
9 TableHeader, TableStateHandle, Text,
10};
11use strato_ui::fonts::FamilyId;
12use strato_ui::keymap::FixedBinding;
13use strato_ui::platform::CapturedFrame;
14use strato_ui::presenter::ChildView;
15use strato_ui::SingletonEntity as _;
16use strato_ui::{
17 AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle, WindowId,
18};
19 
20const TOTAL_DEMOS: usize = 13;
21 
22#[derive(Debug, Clone)]
23pub enum SampleAction {
24 NextDemo,
25 PreviousDemo,
26}
27 
28pub fn init(ctx: &mut AppContext) {
29 use strato_ui::keymap::macros::*;
30 
31 ctx.register_fixed_bindings([
32 FixedBinding::new("right", SampleAction::NextDemo, id!("TableSampleView")),
33 FixedBinding::new("left", SampleAction::PreviousDemo, id!("TableSampleView")),
34 ]);
35}
36 
37pub struct RootView {
38 sub_view: ViewHandle<TableSampleView>,
39}
40 
41impl RootView {
42 pub fn new(ctx: &mut ViewContext<Self>, capture_config: CaptureConfig) -> Self {
43 let config_clone = capture_config.clone();
44 let sub_view = ctx.add_typed_action_view(move |ctx| {
45 let font_family = strato_ui::fonts::Cache::handle(ctx)
46 .update(ctx, |cache, _| cache.load_system_font("Arial").unwrap());
47 ctx.focus_self();
48 TableSampleView::new(font_family, config_clone.clone(), ctx)
49 });
50 Self { sub_view }
51 }
52}
53 
54impl Entity for RootView {
55 type Event = ();
56}
57 
58impl View for RootView {
59 fn ui_name() -> &'static str {
60 "RootView"
61 }
62 
63 fn render(&self, _app: &AppContext) -> Box<dyn Element> {
64 ChildView::new(&self.sub_view).finish()
65 }
66}
67 
68impl TypedActionView for RootView {
69 type Action = ();
70}
71 
72pub struct TableSampleView {
73 font_family: FamilyId,
74 current_demo: usize,
75 table_states: Vec<TableStateHandle>,
76 scroll_state: ScrollStateHandle,
77 scroll_state_fixed_header: ScrollStateHandle,
78 scroll_state_padding: ClippedScrollStateHandle,
79 scroll_state_edge_cases: ClippedScrollStateHandle,
80 scroll_state_varying_heights: ScrollStateHandle,
81 scroll_state_fixed_header_mixed_columns: ScrollStateHandle,
82 selection_handle_1: SelectionHandle,
83 selection_handle_2: SelectionHandle,
84 selection_handle_virtualized: SelectionHandle,
85 selection_handle_fixed_header: SelectionHandle,
86 selection_handle_varying_heights: SelectionHandle,
87 selection_handle_fixed_header_mixed_columns: SelectionHandle,
88 capture_config: CaptureConfig,
89 window_id: WindowId,
90 captured_count: Arc<Mutex<usize>>,
91}
92 
93impl TableSampleView {
94 fn new(
95 font_family: FamilyId,
96 capture_config: CaptureConfig,
97 ctx: &mut ViewContext<Self>,
98 ) -> Self {
99 let window_id = ctx.window_id();
100 let captured_count = Arc::new(Mutex::new(0));
101 
102 // Auto-start capture sequence if enabled
103 let should_auto_capture = capture_config.capture_screenshots;
104 
105 let view = Self {
106 font_family,
107 current_demo: 0,
108 table_states: (0..TOTAL_DEMOS)
109 .map(|_| TableStateHandle::new(0, |_, _| vec![]))
110 .collect(),
111 scroll_state: ScrollStateHandle::default(),
112 scroll_state_fixed_header: ScrollStateHandle::default(),
113 scroll_state_padding: ClippedScrollStateHandle::default(),
114 scroll_state_edge_cases: ClippedScrollStateHandle::default(),
115 scroll_state_varying_heights: ScrollStateHandle::default(),
116 scroll_state_fixed_header_mixed_columns: ScrollStateHandle::default(),
117 selection_handle_1: SelectionHandle::default(),
118 selection_handle_2: SelectionHandle::default(),
119 selection_handle_virtualized: SelectionHandle::default(),
120 selection_handle_fixed_header: SelectionHandle::default(),
121 selection_handle_varying_heights: SelectionHandle::default(),
122 selection_handle_fixed_header_mixed_columns: SelectionHandle::default(),
123 capture_config,
124 window_id,
125 captured_count,
126 };
127 
128 // Trigger automated capture sequence driven from a background future
129 if should_auto_capture {
130 let spawner = ctx.spawner();
131 ctx.spawn(
132 async move {
133 use strato_ui::r#async::Timer;
134 Timer::after(std::time::Duration::from_millis(800)).await;
135 for i in 0..TOTAL_DEMOS {
136 let _ = spawner
137 .spawn(move |view, ctx| {
138 view.current_demo = i;
139 ctx.notify();
140 })
141 .await;
142 Timer::after(std::time::Duration::from_millis(300)).await;
143 let _ = spawner
144 .spawn(|view, ctx| {
145 view.capture_current_demo(ctx);
146 })
147 .await;
148 Timer::after(std::time::Duration::from_millis(250)).await;
149 }
150 },
151 |_, _, _| {},
152 );
153 }
154 
155 view
156 }
157 
158 fn capture_current_demo(&mut self, ctx: &mut ViewContext<Self>) {
159 let demo_names = [
160 "01_column_widths",
161 "02_virtualized",
162 "03_virtualized_fixed_header",
163 "04_selection",
164 "05_selection_virtualized",
165 "06_text_wrapping",
166 "07_mixed_elements",
167 "08_padding",
168 "09_banding",
169 "10_theming",
170 "11_edge_cases",
171 "12_varying_heights",
172 "13_fixed_header_mixed_columns",
173 ];
174 
175 let demo_name = demo_names[self.current_demo].to_string();
176 let is_baseline = self.capture_config.capture_baseline;
177 let captured_count = Arc::clone(&self.captured_count);
178 
179 if let Some(window) = ctx.windows().platform_window(self.window_id) {
180 println!(
181 "📸 Capturing demo {}/{}: {}",
182 self.current_demo + 1,
183 TOTAL_DEMOS,
184 demo_name
185 );
186 let is_last_demo = self.current_demo >= TOTAL_DEMOS - 1;
187 let auto_capture = self.capture_config.capture_screenshots;
188 
189 window
190 .as_ctx()
191 .request_frame_capture(Box::new(move |frame| {
192 let dir = if is_baseline {
193 "screenshots/baseline"
194 } else {
195 "screenshots/current"
196 };
197 let _ = std::fs::create_dir_all(dir);
198 let filename = format!("{}/{}.png", dir, demo_name);
199 if let Err(e) = save_frame_as_png(&frame, &filename) {
200 eprintln!("❌ Failed to save {}: {}", filename, e);
201 return;
202 }
203 println!("✅ Saved: {}", filename);
204 let mut count = captured_count.lock().unwrap();
205 *count += 1;
206 if *count >= TOTAL_DEMOS {
207 println!("\n🎉 All {} screenshots captured!", TOTAL_DEMOS);
208 std::process::exit(0);
209 }
210 // Next demo will be scheduled below using a timer on the main thread
211 }));
212 
213 if auto_capture && !is_last_demo {
214 ctx.spawn(
215 async {
216 strato_ui::r#async::Timer::after(std::time::Duration::from_millis(350))
217 .await;
218 },
219 |_, _, ctx| {
220 ctx.dispatch_typed_action(&SampleAction::NextDemo);
221 },
222 );
223 }
224 }
225 }
226 
227 fn create_header(&self, text: &str) -> TableHeader {
228 TableHeader::new(
229 Text::new(text.to_string(), self.font_family, 14.0)
230 .with_color(ColorU::new(30, 30, 30, 255))
231 .finish(),
232 )
233 }
234 
235 fn create_header_with_width(&self, text: &str, width: TableColumnWidth) -> TableHeader {
236 TableHeader::new(
237 Text::new(text.to_string(), self.font_family, 14.0)
238 .with_color(ColorU::new(30, 30, 30, 255))
239 .finish(),
240 )
241 .with_width(width)
242 }
243 
244 fn render_demo_header(&self, title: &str, description: &str) -> Box<dyn Element> {
245 Flex::column()
246 .with_spacing(8.0)
247 .with_child(
248 Text::new(
249 format!("Demo {}/{} - {}", self.current_demo + 1, TOTAL_DEMOS, title),
250 self.font_family,
251 24.0,
252 )
253 .with_color(ColorU::white())
254 .finish(),
255 )
256 .with_child(
257 Text::new(description.to_string(), self.font_family, 14.0)
258 .with_color(ColorU::new(180, 180, 180, 255))
259 .finish(),
260 )
261 .with_child(
262 Text::new(
263 "Use ← → arrow keys to navigate between demos".to_string(),
264 self.font_family,
265 12.0,
266 )
267 .with_color(ColorU::new(120, 120, 120, 255))
268 .finish(),
269 )
270 .finish()
271 }
272 
273 fn render_demo_column_widths(&self) -> Box<dyn Element> {
274 let font_family = self.font_family;
275 let table = Table::new(self.table_states[0].clone(), 800.0, 500.0)
276 .with_headers(vec![
277 self.create_header_with_width("Fixed(100)", TableColumnWidth::Fixed(100.0)),
278 self.create_header_with_width("Flex(1)", TableColumnWidth::Flex(1.0)),
279 self.create_header_with_width("Flex(2)", TableColumnWidth::Flex(2.0)),
280 self.create_header_with_width("Fraction(0.15)", TableColumnWidth::Fraction(0.15)),
281 self.create_header_with_width("Intrinsic", TableColumnWidth::Intrinsic),
282 ])
283 .with_row_count(2)
284 .with_row_render_fn(move |row_idx, _app| {
285 let row_data: &[(&str, &str, &str, &str, &str)] = &[
286 ("100px", "1 part", "2 parts", "15%", "Auto-sized content"),
287 (
288 "Fixed",
289 "Flexible",
290 "More flexible",
291 "Percent",
292 "Fits content width",
293 ),
294 ];
295 let (a, b, c, d, e) = row_data[row_idx];
296 vec![
297 Text::new(a.to_string(), font_family, 14.0)
298 .with_color(ColorU::new(50, 50, 50, 255))
299 .finish(),
300 Text::new(b.to_string(), font_family, 14.0)
301 .with_color(ColorU::new(50, 50, 50, 255))
302 .finish(),
303 Text::new(c.to_string(), font_family, 14.0)
304 .with_color(ColorU::new(50, 50, 50, 255))
305 .finish(),
306 Text::new(d.to_string(), font_family, 14.0)
307 .with_color(ColorU::new(50, 50, 50, 255))
308 .finish(),
309 Text::new(e.to_string(), font_family, 14.0)
310 .with_color(ColorU::new(50, 50, 50, 255))
311 .finish(),
312 ]
313 })
314 .with_config(TableConfig::default())
315 .finish();
316 
317 Flex::column()
318 .with_spacing(20.0)
319 .with_child(self.render_demo_header(
320 "Column Width Types",
321 "Demonstrates Fixed, Flex, Fraction, and Intrinsic column widths",
322 ))
323 .with_child(Container::new(table).with_uniform_padding(10.0).finish())
324 .finish()
325 }
326 
327 fn render_demo_virtualized(&self) -> Box<dyn Element> {
328 let row_count = 1000;
329 let font_family = self.font_family;
330 
331 // Use on-demand row rendering - rows are created lazily during layout
332 let table = Table::new(self.table_states[1].clone(), 800.0, 500.0)
333 .with_headers(vec![
334 self.create_header_with_width("ID", TableColumnWidth::Fixed(80.0)),
335 self.create_header("Column A"),
336 self.create_header("Column B"),
337 self.create_header_with_width("Value", TableColumnWidth::Fixed(100.0)),
338 ])
339 .with_row_count(row_count)
340 .with_row_render_fn(move |row_idx, _app| {
341 vec![
342 Text::new(format!("Row {}", row_idx), font_family, 14.0)
343 .with_color(ColorU::new(50, 50, 50, 255))
344 .finish(),
345 Text::new(format!("Data A-{}", row_idx), font_family, 14.0)
346 .with_color(ColorU::new(50, 50, 50, 255))
347 .finish(),
348 Text::new(format!("Data B-{}", row_idx), font_family, 14.0)
349 .with_color(ColorU::new(50, 50, 50, 255))
350 .finish(),
351 Text::new(format!("Value: {}", row_idx * 10), font_family, 14.0)
352 .with_color(ColorU::new(50, 50, 50, 255))
353 .finish(),
354 ]
355 })
356 .finish_scrollable();
357 
358 let scrollable = Scrollable::vertical(
359 self.scroll_state.clone(),
360 table,
361 ScrollbarWidth::Auto,
362 ColorU::new(60, 60, 60, 255).into(),
363 ColorU::new(80, 80, 80, 255).into(),
364 ColorU::new(100, 100, 100, 255).into(),
365 );
366 
367 let selectable = SelectableArea::new(
368 self.selection_handle_virtualized.clone(),
369 |selection_args, _, _| {
370 if let Some(text) = &selection_args.selection {
371 println!("Virtualized table selected: {:?}", text);
372 }
373 },
374 scrollable.finish(),
375 );
376 
377 Flex::column()
378 .with_spacing(20.0)
379 .with_child(self.render_demo_header(
380 "Virtualized Table (Scrolling Header)",
381 "1000 rows with scroll-based virtualization. Header scrolls with body.",
382 ))
383 .with_child(
384 ConstrainedBox::new(
385 Container::new(selectable.finish())
386 .with_uniform_padding(10.0)
387 .finish(),
388 )
389 .with_height(500.0)
390 .finish(),
391 )
392 .finish()
393 }
394 
395 fn render_demo_virtualized_fixed_header(&self) -> Box<dyn Element> {
396 let row_count = 1000;
397 let font_family = self.font_family;
398 
399 let config = TableConfig {
400 fixed_header: true,
401 ..TableConfig::default()
402 };
403 
404 // Use on-demand row rendering - rows are created lazily during layout
405 let table = Table::new(self.table_states[2].clone(), 800.0, 500.0)
406 .with_headers(vec![
407 self.create_header_with_width("ID", TableColumnWidth::Fixed(80.0)),
408 self.create_header("Column A"),
409 self.create_header("Column B"),
410 self.create_header_with_width("Value", TableColumnWidth::Fixed(100.0)),
411 ])
412 .with_row_count(row_count)
413 .with_row_render_fn(move |row_idx, _app| {
414 vec![
415 Text::new(format!("Row {}", row_idx), font_family, 14.0)
416 .with_color(ColorU::new(50, 50, 50, 255))
417 .finish(),
418 Text::new(format!("Data A-{}", row_idx), font_family, 14.0)
419 .with_color(ColorU::new(50, 50, 50, 255))
420 .finish(),
421 Text::new(format!("Data B-{}", row_idx), font_family, 14.0)
422 .with_color(ColorU::new(50, 50, 50, 255))
423 .finish(),
424 Text::new(format!("Value: {}", row_idx * 10), font_family, 14.0)
425 .with_color(ColorU::new(50, 50, 50, 255))
426 .finish(),
427 ]
428 })
429 .with_config(config)
430 .finish_scrollable();
431 
432 let scrollable = Scrollable::vertical(
433 self.scroll_state_fixed_header.clone(),
434 table,
435 ScrollbarWidth::Auto,
436 ColorU::new(60, 60, 60, 255).into(),
437 ColorU::new(80, 80, 80, 255).into(),
438 ColorU::new(100, 100, 100, 255).into(),
439 );
440 
441 let selectable = SelectableArea::new(
442 self.selection_handle_fixed_header.clone(),
443 |selection_args, _, _| {
444 if let Some(text) = &selection_args.selection {
445 println!("Fixed header table selected: {:?}", text);
446 }
447 },
448 scrollable.finish(),
449 );
450 
451 Flex::column()
452 .with_spacing(20.0)
453 .with_child(self.render_demo_header(
454 "Virtualized Table (Fixed Header)",
455 "1000 rows with scroll-based virtualization. Header stays fixed at top.",
456 ))
457 .with_child(
458 ConstrainedBox::new(
459 Container::new(selectable.finish())
460 .with_uniform_padding(10.0)
461 .finish(),
462 )
463 .with_height(500.0)
464 .finish(),
465 )
466 .finish()
467 }
468 
469 fn render_demo_selection(&self) -> Box<dyn Element> {
470 let font_family = self.font_family;
471 let table = Table::new(self.table_states[3].clone(), 800.0, 500.0)
472 .with_headers(vec![
473 self.create_header("Name"),
474 self.create_header("Email"),
475 self.create_header("Role"),
476 ])
477 .with_row_count(5)
478 .with_row_render_fn(move |row_idx, _app| {
479 let row_data: &[(&str, &str, &str)] = &[
480 ("Alice Johnson", "alice@example.com", "Engineer"),
481 ("Bob Smith", "bob@example.com", "Designer"),
482 ("Carol Williams", "carol@example.com", "Manager"),
483 ("David Brown", "david@example.com", "Engineer"),
484 ("Eve Davis", "eve@example.com", "Designer"),
485 ];
486 let (name, email, role) = row_data[row_idx];
487 vec![
488 Text::new(name.to_string(), font_family, 14.0)
489 .with_color(ColorU::new(50, 50, 50, 255))
490 .finish(),
491 Text::new(email.to_string(), font_family, 14.0)
492 .with_color(ColorU::new(50, 50, 50, 255))
493 .finish(),
494 Text::new(role.to_string(), font_family, 14.0)
495 .with_color(ColorU::new(50, 50, 50, 255))
496 .finish(),
497 ]
498 })
499 .with_config(TableConfig::default())
500 .finish();
501 
502 let selectable = SelectableArea::new(
503 self.selection_handle_1.clone(),
504 |selection_args, _, _| {
505 if let Some(text) = &selection_args.selection {
506 println!("Selected: {:?}", text);
507 }
508 },
509 table,
510 );
511 
512 Flex::column()
513 .with_spacing(20.0)
514 .with_child(self.render_demo_header(
515 "Selection (Non-Virtualized)",
516 "Click and drag to select text. Selection is logged to console.",
517 ))
518 .with_child(
519 Container::new(selectable.finish())
520 .with_uniform_padding(10.0)
521 .finish(),
522 )
523 .finish()
524 }
525 
526 fn render_demo_selection_virtualized(&self) -> Box<dyn Element> {
527 let font_family = self.font_family;
528 let table = Table::new(self.table_states[4].clone(), 800.0, 500.0)
529 .with_headers(vec![
530 self.create_header("Item"),
531 self.create_header("Description"),
532 self.create_header_with_width("Price", TableColumnWidth::Fixed(80.0)),
533 ])
534 .with_row_count(50)
535 .with_row_render_fn(move |row_idx, _app| {
536 vec![
537 Text::new(format!("Item {}", row_idx), font_family, 14.0)
538 .with_color(ColorU::new(50, 50, 50, 255))
539 .finish(),
540 Text::new(
541 format!("Description for item {}", row_idx),
542 font_family,
543 14.0,
544 )
545 .with_color(ColorU::new(50, 50, 50, 255))
546 .finish(),
547 Text::new(
548 format!("${:.2}", (row_idx as f32) * 9.99),
549 font_family,
550 14.0,
551 )
552 .with_color(ColorU::new(50, 50, 50, 255))
553 .finish(),
554 ]
555 })
556 .with_config(TableConfig::default())
557 .finish();
558 
559 let selectable = SelectableArea::new(
560 self.selection_handle_2.clone(),
561 |selection_args, _, _| {
562 if let Some(text) = &selection_args.selection {
563 println!("Virtualized selection: {:?}", text);
564 }
565 },
566 table,
567 );
568 
569 Flex::column()
570 .with_spacing(20.0)
571 .with_child(self.render_demo_header(
572 "Selection (Virtualized)",
573 "50 rows with visible range 5-25. Selection includes gap placeholders.",
574 ))
575 .with_child(
576 Container::new(selectable.finish())
577 .with_uniform_padding(10.0)
578 .finish(),
579 )
580 .finish()
581 }
582 
583 fn render_demo_text_wrapping(&self) -> Box<dyn Element> {
584 let font_family = self.font_family;
585 let table = Table::new(self.table_states[5].clone(), 800.0, 500.0)
586 .with_headers(vec![
587 self.create_header_with_width("Title", TableColumnWidth::Fixed(120.0)),
588 self.create_header("Description"),
589 self.create_header_with_width("Status", TableColumnWidth::Fixed(80.0)),
590 ])
591 .with_row_count(3)
592 .with_row_render_fn(move |row_idx, _app| {
593 let row_data: &[(&str, &str, &str)] = &[
594 ("Short", "A brief description", "Done"),
595 ("Medium Length", "This is a longer description that should wrap to multiple lines when the column width is constrained.", "In Progress"),
596 ("Very Long Title Here", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Pending"),
597 ];
598 let (title, desc, status) = row_data[row_idx];
599 vec![
600 Text::new(title.to_string(), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
601 Text::new(desc.to_string(), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
602 Text::new(status.to_string(), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
603 ]
604 })
605 .with_config(TableConfig::default())
606 .finish();
607 
608 Flex::column()
609 .with_spacing(20.0)
610 .with_child(self.render_demo_header(
611 "Rich Cells - Text Wrapping",
612 "Cells with varying text lengths. Row heights adjust to fit content.",
613 ))
614 .with_child(
615 ConstrainedBox::new(Container::new(table).with_uniform_padding(10.0).finish())
616 .with_width(600.0)
617 .finish(),
618 )
619 .finish()
620 }
621 
622 fn render_demo_mixed_elements(&self) -> Box<dyn Element> {
623 let font_family = self.font_family;
624 let table = Table::new(self.table_states[6].clone(), 800.0, 500.0)
625 .with_headers(vec![
626 self.create_header("Icon + Text"),
627 self.create_header("Emojis"),
628 self.create_header("Colored"),
629 ])
630 .with_row_count(4)
631 .with_row_render_fn(move |row_idx, _app| {
632 let row_data: &[(&str, &str, &str, ColorU)] = &[
633 (
634 "📁 Documents",
635 "🎉 🎊 🥳",
636 "Success",
637 ColorU::new(34, 139, 34, 255),
638 ),
639 (
640 "📷 Photos",
641 "❤️ 💙 💚 💛",
642 "Warning",
643 ColorU::new(255, 165, 0, 255),
644 ),
645 (
646 "🎵 Music",
647 "🌟 ⭐ 💫 ✨",
648 "Error",
649 ColorU::new(220, 20, 60, 255),
650 ),
651 (
652 "🎬 Videos",
653 "🚀 🛸 🌙 🌍",
654 "Info",
655 ColorU::new(30, 144, 255, 255),
656 ),
657 ];
658 let (icon_text, emojis, status, color) = row_data[row_idx];
659 vec![
660 Text::new(icon_text.to_string(), font_family, 14.0)
661 .with_color(ColorU::new(50, 50, 50, 255))
662 .finish(),
663 Text::new(emojis.to_string(), font_family, 14.0)
664 .with_color(ColorU::new(50, 50, 50, 255))
665 .finish(),
666 Text::new(status.to_string(), font_family, 14.0)
667 .with_color(color)
668 .finish(),
669 ]
670 })
671 .with_config(TableConfig::default())
672 .finish();
673 
674 Flex::column()
675 .with_spacing(20.0)
676 .with_child(self.render_demo_header(
677 "Rich Cells - Mixed Elements",
678 "Cells with emojis, icons, and colored text",
679 ))
680 .with_child(Container::new(table).with_uniform_padding(10.0).finish())
681 .finish()
682 }
683 
684 fn render_demo_padding(&self) -> Box<dyn Element> {
685 let font_family = self.font_family;
686 let create_table = move |state: TableStateHandle, padding: f32, ff: FamilyId| {
687 let config = TableConfig {
688 cell_padding: padding,
689 ..TableConfig::default()
690 };
691 Table::new(state, 800.0, 500.0)
692 .with_headers(vec![
693 TableHeader::new(
694 Text::new("A".to_string(), ff, 14.0)
695 .with_color(ColorU::new(30, 30, 30, 255))
696 .finish(),
697 ),
698 TableHeader::new(
699 Text::new("B".to_string(), ff, 14.0)
700 .with_color(ColorU::new(30, 30, 30, 255))
701 .finish(),
702 ),
703 ])
704 .with_row_count(2)
705 .with_row_render_fn(move |row_idx, _app| {
706 let cells = ["Cell 1", "Cell 2", "Cell 3", "Cell 4"];
707 vec![
708 Text::new(cells[row_idx * 2].to_string(), ff, 14.0)
709 .with_color(ColorU::new(50, 50, 50, 255))
710 .finish(),
711 Text::new(cells[row_idx * 2 + 1].to_string(), ff, 14.0)
712 .with_color(ColorU::new(50, 50, 50, 255))
713 .finish(),
714 ]
715 })
716 .with_config(config)
717 .finish()
718 };
719 
720 let tables_row = Flex::row()
721 .with_spacing(20.0)
722 .with_child(
723 Flex::column()
724 .with_spacing(8.0)
725 .with_child(
726 Text::new_inline("4px padding", self.font_family, 12.0)
727 .with_color(ColorU::white())
728 .finish(),
729 )
730 .with_child(create_table(self.table_states[7].clone(), 4.0, font_family))
731 .finish(),
732 )
733 .with_child(
734 Flex::column()
735 .with_spacing(8.0)
736 .with_child(
737 Text::new_inline("8px padding (default)", self.font_family, 12.0)
738 .with_color(ColorU::white())
739 .finish(),
740 )
741 .with_child(create_table(
742 TableStateHandle::new(0, |_, _| vec![]),
743 8.0,
744 font_family,
745 ))
746 .finish(),
747 )
748 .with_child(
749 Flex::column()
750 .with_spacing(8.0)
751 .with_child(
752 Text::new_inline("16px padding", self.font_family, 12.0)
753 .with_color(ColorU::white())
754 .finish(),
755 )
756 .with_child(create_table(
757 TableStateHandle::new(0, |_, _| vec![]),
758 16.0,
759 font_family,
760 ))
761 .finish(),
762 )
763 .finish();
764 
765 let scrollable = ClippedScrollable::horizontal(
766 self.scroll_state_padding.clone(),
767 tables_row,
768 ScrollbarWidth::Auto,
769 ColorU::new(60, 60, 60, 255).into(),
770 ColorU::new(80, 80, 80, 255).into(),
771 ColorU::new(100, 100, 100, 255).into(),
772 );
773 
774 Flex::column()
775 .with_spacing(20.0)
776 .with_child(self.render_demo_header(
777 "Cell Padding Comparison",
778 "Same table with different cell_padding values: 4px, 8px (default), 16px. Scroll horizontally if needed.",
779 ))
780 .with_child(scrollable.finish())
781 .finish()
782 }
783 
784 fn render_demo_banding(&self) -> Box<dyn Element> {
785 let font_family = self.font_family;
786 let config = TableConfig {
787 row_background: RowBackground::striped(
788 ColorU::white(),
789 ColorU::new(240, 245, 250, 255),
790 ),
791 ..TableConfig::default()
792 };
793 
794 let table = Table::new(self.table_states[8].clone(), 800.0, 500.0)
795 .with_headers(vec![
796 self.create_header("#"),
797 self.create_header("Product"),
798 self.create_header("Category"),
799 self.create_header("Price"),
800 ])
801 .with_row_count(6)
802 .with_row_render_fn(move |row_idx, _app| {
803 let row_data: &[(&str, &str, &str, &str)] = &[
804 ("1", "Widget A", "Electronics", "$29.99"),
805 ("2", "Gadget B", "Electronics", "$49.99"),
806 ("3", "Tool C", "Hardware", "$19.99"),
807 ("4", "Device D", "Electronics", "$99.99"),
808 ("5", "Part E", "Hardware", "$9.99"),
809 ("6", "Component F", "Electronics", "$39.99"),
810 ];
811 let (num, product, category, price) = row_data[row_idx];
812 vec![
813 Text::new(num.to_string(), font_family, 14.0)
814 .with_color(ColorU::new(50, 50, 50, 255))
815 .finish(),
816 Text::new(product.to_string(), font_family, 14.0)
817 .with_color(ColorU::new(50, 50, 50, 255))
818 .finish(),
819 Text::new(category.to_string(), font_family, 14.0)
820 .with_color(ColorU::new(50, 50, 50, 255))
821 .finish(),
822 Text::new(price.to_string(), font_family, 14.0)
823 .with_color(ColorU::new(50, 50, 50, 255))
824 .finish(),
825 ]
826 })
827 .with_config(config)
828 .finish();
829 
830 Flex::column()
831 .with_spacing(20.0)
832 .with_child(self.render_demo_header(
833 "Alternate Row Banding",
834 "Zebra striping with alternate_row_background set",
835 ))
836 .with_child(Container::new(table).with_uniform_padding(10.0).finish())
837 .finish()
838 }
839 
840 fn render_demo_theming(&self) -> Box<dyn Element> {
841 let font_family = self.font_family;
842 let dark_config = TableConfig {
843 border_width: 2.0,
844 border_color: ColorU::new(80, 80, 80, 255),
845 cell_padding: 12.0,
846 header_background: ColorU::new(45, 45, 45, 255),
847 row_background: RowBackground::striped(
848 ColorU::new(30, 30, 30, 255),
849 ColorU::new(40, 40, 40, 255),
850 ),
851 ..TableConfig::default()
852 };
853 
854 let create_dark_header = |text: &str, ff: FamilyId| {
855 TableHeader::new(
856 Text::new(text.to_string(), ff, 14.0)
857 .with_color(ColorU::new(220, 220, 220, 255))
858 .finish(),
859 )
860 };
861 
862 let table = Table::new(self.table_states[9].clone(), 800.0, 500.0)
863 .with_headers(vec![
864 create_dark_header("Property", font_family),
865 create_dark_header("Value", font_family),
866 create_dark_header("Description", font_family),
867 ])
868 .with_row_count(4)
869 .with_row_render_fn(move |row_idx, _app| {
870 let row_data: &[(&str, &str, &str)] = &[
871 ("border_width", "2.0", "Thicker borders"),
872 ("border_color", "#505050", "Dark gray borders"),
873 ("header_background", "#2D2D2D", "Dark header"),
874 ("row_background", "#1E1E1E", "Very dark rows"),
875 ];
876 let (prop, val, desc) = row_data[row_idx];
877 vec![
878 Text::new(prop.to_string(), font_family, 14.0)
879 .with_color(ColorU::new(200, 200, 200, 255))
880 .finish(),
881 Text::new(val.to_string(), font_family, 14.0)
882 .with_color(ColorU::new(200, 200, 200, 255))
883 .finish(),
884 Text::new(desc.to_string(), font_family, 14.0)
885 .with_color(ColorU::new(200, 200, 200, 255))
886 .finish(),
887 ]
888 })
889 .with_config(dark_config)
890 .finish();
891 
892 Flex::column()
893 .with_spacing(20.0)
894 .with_child(self.render_demo_header(
895 "Border and Colors (Dark Theme)",
896 "Custom theming with modified border, header, and row colors",
897 ))
898 .with_child(Container::new(table).with_uniform_padding(10.0).finish())
899 .finish()
900 }
901 
902 fn render_demo_edge_cases(&self) -> Box<dyn Element> {
903 let font_family = self.font_family;
904 
905 let empty_table = Table::new(TableStateHandle::new(0, |_, _| vec![]), 800.0, 500.0)
906 .with_headers(vec![
907 self.create_header("A"),
908 self.create_header("B"),
909 self.create_header("C"),
910 ])
911 .finish();
912 
913 let single_row = Table::new(
914 TableStateHandle::new(1, move |_, _| {
915 vec![
916 Text::new("Single".to_string(), font_family, 14.0)
917 .with_color(ColorU::new(50, 50, 50, 255))
918 .finish(),
919 Text::new("Entry".to_string(), font_family, 14.0)
920 .with_color(ColorU::new(50, 50, 50, 255))
921 .finish(),
922 ]
923 }),
924 800.0,
925 500.0,
926 )
927 .with_headers(vec![self.create_header("Only"), self.create_header("Row")])
928 .finish();
929 
930 let single_column = Table::new(
931 TableStateHandle::new(3, move |row_idx, _| {
932 vec![Text::new(format!("Row {}", row_idx + 1), font_family, 14.0)
933 .with_color(ColorU::new(50, 50, 50, 255))
934 .finish()]
935 }),
936 800.0,
937 500.0,
938 )
939 .with_headers(vec![self.create_header("Single Column")])
940 .finish();
941 
942 let many_columns = Table::new(self.table_states[10].clone(), 800.0, 500.0)
943 .with_headers(
944 (1..=10)
945 .map(|i| {
946 self.create_header_with_width(
947 &format!("Col{}", i),
948 TableColumnWidth::Fixed(60.0),
949 )
950 })
951 .collect(),
952 )
953 .with_row_count(2)
954 .with_row_render_fn(move |row_idx, _| {
955 (1..=10)
956 .map(|i| {
957 Text::new(format!("R{}C{}", row_idx + 1, i), font_family, 14.0)
958 .with_color(ColorU::new(50, 50, 50, 255))
959 .finish()
960 })
961 .collect()
962 })
963 .finish();
964 
965 let tables_row = Flex::row()
966 .with_spacing(20.0)
967 .with_child(
968 Flex::column()
969 .with_spacing(8.0)
970 .with_child(
971 Text::new_inline("Empty (headers only)", self.font_family, 12.0)
972 .with_color(ColorU::white())
973 .finish(),
974 )
975 .with_child(empty_table)
976 .finish(),
977 )
978 .with_child(
979 Flex::column()
980 .with_spacing(8.0)
981 .with_child(
982 Text::new_inline("Single row", self.font_family, 12.0)
983 .with_color(ColorU::white())
984 .finish(),
985 )
986 .with_child(single_row)
987 .finish(),
988 )
989 .with_child(
990 Flex::column()
991 .with_spacing(8.0)
992 .with_child(
993 Text::new_inline("Single column", self.font_family, 12.0)
994 .with_color(ColorU::white())
995 .finish(),
996 )
997 .with_child(single_column)
998 .finish(),
999 )
1000 .finish();
1001 
1002 let scrollable = ClippedScrollable::horizontal(
1003 self.scroll_state_edge_cases.clone(),
1004 tables_row,
1005 ScrollbarWidth::Auto,
1006 ColorU::new(60, 60, 60, 255).into(),
1007 ColorU::new(80, 80, 80, 255).into(),
1008 ColorU::new(100, 100, 100, 255).into(),
1009 );
1010 
1011 Flex::column()
1012 .with_spacing(20.0)
1013 .with_child(self.render_demo_header(
1014 "Edge Cases",
1015 "Empty table, single row, single column, many columns. Scroll horizontally if needed.",
1016 ))
1017 .with_child(scrollable.finish())
1018 .with_child(
1019 Flex::column()
1020 .with_spacing(8.0)
1021 .with_child(
1022 Text::new_inline("Many columns (10)", self.font_family, 12.0)
1023 .with_color(ColorU::white())
1024 .finish(),
1025 )
1026 .with_child(many_columns)
1027 .finish(),
1028 )
1029 .finish()
1030 }
1031 
1032 fn render_demo_virtualized_varying_heights(&self) -> Box<dyn Element> {
1033 let row_count = 500;
1034 let font_family = self.font_family;
1035 
1036 let config = TableConfig {
1037 row_background: RowBackground::striped(
1038 ColorU::white(),
1039 ColorU::new(248, 250, 252, 255),
1040 ),
1041 ..TableConfig::default()
1042 };
1043 
1044 let table = Table::new(self.table_states[TOTAL_DEMOS - 2].clone(), 800.0, 500.0)
1045 .with_headers(vec![
1046 self.create_header_with_width("#", TableColumnWidth::Fixed(60.0)),
1047 self.create_header_with_width("Description", TableColumnWidth::Flex(1.0)),
1048 self.create_header_with_width("Price", TableColumnWidth::Fixed(100.0)),
1049 ])
1050 .with_row_count(row_count)
1051 .with_row_render_fn(move |row_idx, _app| {
1052 let description = match row_idx % 5 {
1053 0 => format!("Row {} - Short", row_idx),
1054 1 => format!("Row {} - This is a medium length description that will wrap to multiple lines in a constrained column", row_idx),
1055 2 => format!("Row {} - Very long description here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.", row_idx),
1056 3 => format!("Row {} - Another short one", row_idx),
1057 4 => format!("Row {} - Medium text here with some more content to make it wrap and create varying row heights throughout the virtualized table.", row_idx),
1058 _ => unreachable!(),
1059 };
1060 vec![
1061 Text::new(format!("{}", row_idx), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1062 Text::new(description, font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1063 Text::new(format!("${}.{:02}", row_idx * 10, (row_idx * 17) % 100), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1064 ]
1065 })
1066 .with_config(config)
1067 .finish_scrollable();
1068 
1069 let scrollable = Scrollable::vertical(
1070 self.scroll_state_varying_heights.clone(),
1071 table,
1072 ScrollbarWidth::Auto,
1073 ColorU::new(60, 60, 60, 255).into(),
1074 ColorU::new(80, 80, 80, 255).into(),
1075 ColorU::new(100, 100, 100, 255).into(),
1076 );
1077 
1078 let selectable = SelectableArea::new(
1079 self.selection_handle_varying_heights.clone(),
1080 |selection_args, _, _| {
1081 if let Some(text) = &selection_args.selection {
1082 println!("Varying heights selection: {:?}", text);
1083 }
1084 },
1085 scrollable.finish(),
1086 );
1087 
1088 Flex::column()
1089 .with_spacing(20.0)
1090 .with_child(self.render_demo_header(
1091 "Virtualized with Varying Heights",
1092 "500 rows with different content lengths. Rows have intrinsic heights based on text wrapping. Try scrolling and selecting text across rows.",
1093 ))
1094 .with_child(
1095 ConstrainedBox::new(
1096 Container::new(selectable.finish())
1097 .with_uniform_padding(10.0)
1098 .finish(),
1099 )
1100 .with_height(500.0)
1101 .finish(),
1102 )
1103 .finish()
1104 }
1105 
1106 fn render_demo_fixed_header_mixed_columns(&self) -> Box<dyn Element> {
1107 let row_count = 500;
1108 let font_family = self.font_family;
1109 
1110 let config = TableConfig {
1111 fixed_header: true,
1112 row_background: RowBackground::striped(
1113 ColorU::white(),
1114 ColorU::new(248, 250, 252, 255),
1115 ),
1116 ..TableConfig::default()
1117 };
1118 
1119 let table = Table::new(self.table_states[TOTAL_DEMOS - 1].clone(), 800.0, 500.0)
1120 .with_headers(vec![
1121 self.create_header_with_width("#", TableColumnWidth::Fixed(50.0)),
1122 self.create_header_with_width("ID", TableColumnWidth::Intrinsic),
1123 self.create_header_with_width("Description", TableColumnWidth::Flex(1.0)),
1124 self.create_header_with_width("Status", TableColumnWidth::Intrinsic),
1125 self.create_header_with_width("Price", TableColumnWidth::Fraction(0.12)),
1126 ])
1127 .with_row_count(row_count)
1128 .with_row_render_fn(move |row_idx, _app| {
1129 let description = match row_idx % 5 {
1130 0 => format!("Row {} - Short", row_idx),
1131 1 => format!("Row {} - This is a medium length description that will wrap to multiple lines in a constrained column", row_idx),
1132 2 => format!("Row {} - Very long description here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.", row_idx),
1133 3 => format!("Row {} - Another short one", row_idx),
1134 4 => format!("Row {} - Medium text here with some more content to make it wrap and create varying row heights throughout the virtualized table.", row_idx),
1135 _ => unreachable!(),
1136 };
1137 let (status, status_color) = match row_idx % 4 {
1138 0 => ("Active", ColorU::new(34, 139, 34, 255)),
1139 1 => ("Pending", ColorU::new(255, 165, 0, 255)),
1140 2 => ("Complete", ColorU::new(30, 144, 255, 255)),
1141 3 => ("Inactive", ColorU::new(128, 128, 128, 255)),
1142 _ => unreachable!(),
1143 };
1144 vec![
1145 Text::new(format!("{}", row_idx), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1146 Text::new(format!("Item-{:04}", row_idx), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1147 Text::new(description, font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1148 Text::new(status.to_string(), font_family, 14.0).with_color(status_color).finish(),
1149 Text::new(format!("${}.{:02}", row_idx * 10, (row_idx * 17) % 100), font_family, 14.0).with_color(ColorU::new(50, 50, 50, 255)).finish(),
1150 ]
1151 })
1152 .with_config(config)
1153 .finish_scrollable();
1154 
1155 let scrollable = Scrollable::vertical(
1156 self.scroll_state_fixed_header_mixed_columns.clone(),
1157 table,
1158 ScrollbarWidth::Auto,
1159 ColorU::new(60, 60, 60, 255).into(),
1160 ColorU::new(80, 80, 80, 255).into(),
1161 ColorU::new(100, 100, 100, 255).into(),
1162 );
1163 
1164 let selectable = SelectableArea::new(
1165 self.selection_handle_fixed_header_mixed_columns.clone(),
1166 |selection_args, _, _| {
1167 if let Some(text) = &selection_args.selection {
1168 println!("Fixed header mixed columns selection: {:?}", text);
1169 }
1170 },
1171 scrollable.finish(),
1172 );
1173 
1174 Flex::column()
1175 .with_spacing(20.0)
1176 .with_child(self.render_demo_header(
1177 "Fixed Header with Mixed Column Types",
1178 "500 rows with fixed header. Columns: Fixed(50), Intrinsic, Flex(1), Intrinsic, Fraction(0.12). Try scrolling - header stays fixed.",
1179 ))
1180 .with_child(
1181 ConstrainedBox::new(
1182 Container::new(selectable.finish())
1183 .with_uniform_padding(10.0)
1184 .finish(),
1185 )
1186 .with_height(500.0)
1187 .finish(),
1188 )
1189 .finish()
1190 }
1191}
1192 
1193impl Entity for TableSampleView {
1194 type Event = ();
1195}
1196 
1197impl View for TableSampleView {
1198 fn ui_name() -> &'static str {
1199 "TableSampleView"
1200 }
1201 
1202 fn render(&self, _app: &AppContext) -> Box<dyn Element> {
1203 let demo_content = match self.current_demo {
1204 0 => self.render_demo_column_widths(),
1205 1 => self.render_demo_virtualized(),
1206 2 => self.render_demo_virtualized_fixed_header(),
1207 3 => self.render_demo_selection(),
1208 4 => self.render_demo_selection_virtualized(),
1209 5 => self.render_demo_text_wrapping(),
1210 6 => self.render_demo_mixed_elements(),
1211 7 => self.render_demo_padding(),
1212 8 => self.render_demo_banding(),
1213 9 => self.render_demo_theming(),
1214 10 => self.render_demo_edge_cases(),
1215 11 => self.render_demo_virtualized_varying_heights(),
1216 12 => self.render_demo_fixed_header_mixed_columns(),
1217 _ => self.render_demo_column_widths(),
1218 };
1219 
1220 Container::new(
1221 Flex::column()
1222 .with_main_axis_size(MainAxisSize::Max)
1223 .with_cross_axis_alignment(strato_ui::elements::CrossAxisAlignment::Stretch)
1224 .with_child(
1225 Container::new(demo_content)
1226 .with_uniform_padding(20.0)
1227 .finish(),
1228 )
1229 .with_child(Box::new(Empty::new()))
1230 .finish(),
1231 )
1232 .with_background(Fill::Solid(ColorU::new(40, 44, 52, 255)))
1233 .finish()
1234 }
1235}
1236 
1237impl TypedActionView for TableSampleView {
1238 type Action = SampleAction;
1239 
1240 fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
1241 match action {
1242 SampleAction::NextDemo => {
1243 self.current_demo = (self.current_demo + 1) % TOTAL_DEMOS;
1244 
1245 // Trigger capture and auto-advance if in capture mode
1246 if self.capture_config.capture_screenshots {
1247 ctx.spawn(
1248 async {
1249 // Wait for render to complete
1250 strato_ui::r#async::Timer::after(std::time::Duration::from_millis(300))
1251 .await;
1252 },
1253 |view, _, ctx| {
1254 view.capture_current_demo(ctx);
1255 },
1256 );
1257 }
1258 }
1259 SampleAction::PreviousDemo => {
1260 self.current_demo = if self.current_demo == 0 {
1261 TOTAL_DEMOS - 1
1262 } else {
1263 self.current_demo - 1
1264 };
1265 }
1266 }
1267 ctx.notify();
1268 }
1269}
1270 
1271fn save_frame_as_png(frame: &CapturedFrame, path: &str) -> Result<(), Box<dyn std::error::Error>> {
1272 let file = std::fs::File::create(path)?;
1273 let mut writer = std::io::BufWriter::new(file);
1274 
1275 let encoder = image::codecs::png::PngEncoder::new_with_quality(
1276 &mut writer,
1277 image::codecs::png::CompressionType::Fast,
1278 image::codecs::png::FilterType::NoFilter,
1279 );
1280 
1281 encoder.write_image(
1282 &frame.data,
1283 frame.width,
1284 frame.height,
1285 image::ColorType::Rgba8.into(),
1286 )?;
1287 
1288 Ok(())
1289}
1290