Skip to main content

37_error_boundary/
37_error_boundary.rs

1//! Example 37: Error Boundary — Crash Protection
2//!
3//! A component that deliberately panics when a counter reaches 5.
4//! The error boundary catches the panic and renders a fallback.
5//!
6//! Run with: `cargo run -p telex-tui --example 37_error_boundary`
7
8use crossterm::event::KeyCode;
9use crossterm::style::Color;
10use telex::buffer::{Buffer, Rect};
11use telex::prelude::*;
12use telex::widget::Widget;
13
14telex::require_api!(0, 2);
15
16fn main() {
17    telex::run(App).unwrap();
18}
19
20/// A widget that panics at render time when the counter reaches 5.
21struct RiskyCounter(i32);
22
23impl Widget for RiskyCounter {
24    fn render(&self, area: Rect, buf: &mut Buffer) {
25        assert!(self.0 < 5, "Counter hit 5 — boom!");
26        let text = format!("Counter: {} (panics at 5)", self.0);
27        buf.write_str(area.x, area.y, &text, Color::Green, Color::Reset);
28    }
29
30    fn height_hint(&self, _width: u16) -> Option<u16> {
31        Some(1)
32    }
33}
34
35struct App;
36
37impl Component for App {
38    fn render(&self, cx: Scope) -> View {
39        let show_help = state!(cx, || false);
40
41        cx.use_command(
42            KeyBinding::key(KeyCode::F(1)),
43            with!(show_help => move || show_help.update(|v| *v = !*v)),
44        );
45
46        let count = state!(cx, || 0i32);
47
48        // The panic must happen at render time (inside render_view) so the
49        // error boundary's catch_unwind can catch it. A custom widget defers
50        // execution to the render pass.
51        let risky_view = View::custom(std::rc::Rc::new(std::cell::RefCell::new(RiskyCounter(count.get()))));
52
53        let fallback = View::vstack()
54            .child(View::styled_text("CAUGHT PANIC").color(Color::Red).bold().build())
55            .child(View::text("The child view panicked."))
56            .child(View::text("But the app is still running!"))
57            .build();
58
59        View::vstack()
60            .spacing(1)
61            .child(View::styled_text("Error Boundary Demo").bold().build())
62            .child(
63                View::hstack()
64                    .spacing(2)
65                    .child(
66                        View::vstack()
67                            .spacing(1)
68                            .child(View::styled_text("Protected Panel").bold().build())
69                            .child(
70                                View::error_boundary()
71                                    .child(risky_view)
72                                    .fallback(fallback)
73                                    .build(),
74                            )
75                            .build(),
76                    )
77                    .child(
78                        View::vstack()
79                            .spacing(1)
80                            .child(View::styled_text("How It Works").bold().build())
81                            .child(View::text("The left panel asserts"))
82                            .child(View::text("count < 5. When it hits"))
83                            .child(View::text("5, the error boundary"))
84                            .child(View::text("catches the panic and"))
85                            .child(View::text("renders the fallback."))
86                            .build(),
87                    )
88                    .build(),
89            )
90            .child(
91                View::hstack()
92                    .spacing(1)
93                    .child(
94                        View::button()
95                            .label("[ + Increment ]")
96                            .on_press(with!(count => move || count.update(|n| *n += 1)))
97                            .build(),
98                    )
99                    .child(
100                        View::button()
101                            .label("[ Reset to 0 ]")
102                            .on_press(with!(count => move || count.set(0)))
103                            .build(),
104                    )
105                    .child(View::styled_text(format!("count = {}", count.get())).dim().build())
106                    .build(),
107            )
108            .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
109            .child(
110                View::modal()
111                    .visible(show_help.get())
112                    .title("Example 37: Error Boundary")
113                    .on_dismiss(with!(show_help => move || show_help.set(false)))
114                    .child(
115                        View::vstack()
116                            .child(View::styled_text("What you're seeing").bold().build())
117                            .child(View::text("• A counter that panics at 5"))
118                            .child(View::text("• Error boundary catches the panic"))
119                            .child(View::text("• Red fallback replaces the crash"))
120                            .child(View::gap(1))
121                            .child(View::styled_text("Key concepts").bold().build())
122                            .child(View::text("• View::error_boundary()"))
123                            .child(View::text("• .child(risky) .fallback(safe)"))
124                            .child(View::text("• Panics are caught, not propagated"))
125                            .child(View::text("• App keeps running after panic"))
126                            .child(View::gap(1))
127                            .child(View::styled_text("Try this").bold().build())
128                            .child(View::text("• Increment to 5 to trigger panic"))
129                            .child(View::text("• Reset to 0 to recover"))
130                            .child(View::text("• Keep incrementing past 5"))
131                            .child(View::gap(1))
132                            .child(View::styled_text("Next up").bold().build())
133                            .child(View::text("-> 38_custom_widget: Game of Life"))
134                            .child(View::gap(1))
135                            .child(View::styled_text("Press Escape to close").dim().build())
136                            .build(),
137                    )
138                    .build(),
139            )
140            .build()
141    }
142}