Skip to main content

25_context/
25_context.rs

1//! Example 25: Context
2//!
3//! Demonstrates using provide_context and use_context for sharing
4//! global state like themes, user preferences, or app configuration.
5//!
6//! Run with: `cargo run -p telex-tui --example 25_context`
7
8use crossterm::event::KeyCode;
9use telex::prelude::*;
10use telex::Color;
11
12telex::require_api!(0, 2);
13
14fn main() {
15    telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
16}
17
18// Shared state types that will be provided via context
19#[derive(Clone)]
20struct AppConfig {
21    app_name: String,
22    version: String,
23}
24
25#[derive(Clone, Copy, PartialEq)]
26enum ColorTheme {
27    Default,
28    Ocean,
29    Forest,
30    Sunset,
31}
32
33impl ColorTheme {
34    fn primary(&self) -> Color {
35        match self {
36            ColorTheme::Default => Color::White,
37            ColorTheme::Ocean => Color::Cyan,
38            ColorTheme::Forest => Color::Green,
39            ColorTheme::Sunset => Color::Yellow,
40        }
41    }
42
43    fn accent(&self) -> Color {
44        match self {
45            ColorTheme::Default => Color::Blue,
46            ColorTheme::Ocean => Color::Blue,
47            ColorTheme::Forest => Color::DarkGreen,
48            ColorTheme::Sunset => Color::Red,
49        }
50    }
51
52    fn name(&self) -> &'static str {
53        match self {
54            ColorTheme::Default => "Default",
55            ColorTheme::Ocean => "Ocean",
56            ColorTheme::Forest => "Forest",
57            ColorTheme::Sunset => "Sunset",
58        }
59    }
60}
61
62#[derive(Clone)]
63struct User {
64    name: String,
65    logged_in: bool,
66}
67
68struct App;
69
70impl Component for App {
71    fn render(&self, cx: Scope) -> View {
72        let show_help = state!(cx, || false);
73
74        // F1 toggles help
75        cx.use_command(
76            KeyBinding::key(KeyCode::F(1)),
77            with!(show_help => move || show_help.update(|v| *v = !*v)),
78        );
79
80        // State that we'll provide via context
81        let theme = state!(cx, || ColorTheme::Default);
82        let user = state!(cx, || User {
83            name: "Guest".to_string(),
84            logged_in: false,
85        });
86
87        // Provide static config via context
88        cx.provide_context(AppConfig {
89            app_name: "Context Demo".to_string(),
90            version: "1.0.0".to_string(),
91        });
92
93        // Provide dynamic state via context (current values)
94        cx.provide_context(theme.get());
95        cx.provide_context(user.get());
96
97        // Theme switching handlers
98        let set_default = with!(theme => move || theme.set(ColorTheme::Default));
99        let set_ocean = with!(theme => move || theme.set(ColorTheme::Ocean));
100        let set_forest = with!(theme => move || theme.set(ColorTheme::Forest));
101        let set_sunset = with!(theme => move || theme.set(ColorTheme::Sunset));
102
103        // Login/logout handlers
104        let toggle_login = with!(user => move || {
105            let current = user.get();
106            if current.logged_in {
107                user.set(User {
108                    name: "Guest".to_string(),
109                    logged_in: false,
110                });
111            } else {
112                user.set(User {
113                    name: "Alice".to_string(),
114                    logged_in: true,
115                });
116            }
117        });
118
119        // Get current theme for styling
120        let current_theme = theme.get();
121
122        View::vstack()
123            .spacing(1)
124            // Header - uses context
125            .child(render_header(&cx))
126            .child(
127                // Main content
128                View::boxed()
129                    .flex(1)
130                    .border(true)
131                    .padding(1)
132                    .child(
133                        View::vstack()
134                            .spacing(1)
135                            .child(
136                                View::styled_text("Theme Selection:")
137                                    .color(current_theme.primary())
138                                    .bold()
139                                    .build(),
140                            )
141                            .child(
142                                View::hstack()
143                                    .spacing(2)
144                                    .child(
145                                        View::button()
146                                            .label("Default")
147                                            .on_press(set_default)
148                                            .build(),
149                                    )
150                                    .child(
151                                        View::button().label("Ocean").on_press(set_ocean).build(),
152                                    )
153                                    .child(
154                                        View::button().label("Forest").on_press(set_forest).build(),
155                                    )
156                                    .child(
157                                        View::button().label("Sunset").on_press(set_sunset).build(),
158                                    )
159                                    .build(),
160                            )
161                            .child(View::gap(1))
162                            .child(
163                                View::styled_text("User Actions:")
164                                    .color(current_theme.primary())
165                                    .bold()
166                                    .build(),
167                            )
168                            .child(
169                                View::button()
170                                    .label(if user.get().logged_in {
171                                        "Logout"
172                                    } else {
173                                        "Login as Alice"
174                                    })
175                                    .on_press(toggle_login)
176                                    .build(),
177                            )
178                            .child(View::spacer())
179                            // User info panel - uses context
180                            .child(render_user_panel(&cx))
181                            .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
182                            .build(),
183                    )
184                    .build(),
185            )
186            // Status bar - uses context
187            .child(render_status_bar(&cx))
188            .child(
189                View::modal()
190                    .visible(show_help.get())
191                    .title("Example 25: Context")
192                    .on_dismiss(with!(show_help => move || show_help.set(false)))
193                    .child(
194                        View::vstack()
195                            .child(View::styled_text("What you're seeing").bold().build())
196                            .child(View::text("• Context API for global state"))
197                            .child(View::text("• Theme colors propagate everywhere"))
198                            .child(View::text("• User state shared across components"))
199                            .child(View::gap(1))
200                            .child(View::styled_text("Key concepts").bold().build())
201                            .child(View::text("• cx.provide_context() adds to context"))
202                            .child(View::text("• cx.use_context::<T>() reads context"))
203                            .child(View::text("• Avoids prop drilling"))
204                            .child(View::text("• Great for themes, user, config"))
205                            .child(View::gap(1))
206                            .child(View::styled_text("Try this").bold().build())
207                            .child(View::text("• Switch themes - colors update"))
208                            .child(View::text("• Login/logout - panel updates"))
209                            .child(View::text("• Header and status bar read context"))
210                            .child(View::gap(1))
211                            .child(View::styled_text("Next up").bold().build())
212                            .child(View::text("→ 26_radio_buttons: radio selections"))
213                            .child(View::gap(1))
214                            .child(View::styled_text("Press Escape to close").dim().build())
215                            .build(),
216                    )
217                    .build(),
218            )
219            .build()
220    }
221}
222
223// Helper function that reads from context
224fn render_header(cx: &Scope) -> View {
225    // Read config and theme from context
226    let config = cx.use_context::<AppConfig>();
227    let theme = cx
228        .use_context::<ColorTheme>()
229        .unwrap_or(ColorTheme::Default);
230
231    let title = config
232        .map(|c| format!("{} v{}", c.app_name, c.version))
233        .unwrap_or_else(|| "No config".to_string());
234
235    View::boxed()
236        .border(true)
237        .padding(1)
238        .child(
239            View::vstack()
240                .child(
241                    View::styled_text(title)
242                        .bold()
243                        .color(theme.primary())
244                        .build(),
245                )
246                .child(
247                    View::styled_text("Demonstrates provide_context and use_context")
248                        .dim()
249                        .build(),
250                )
251                .build(),
252        )
253        .build()
254}
255
256// Helper function that reads user from context
257fn render_user_panel(cx: &Scope) -> View {
258    let user = cx.use_context::<User>();
259    let theme = cx
260        .use_context::<ColorTheme>()
261        .unwrap_or(ColorTheme::Default);
262
263    let (status_text, status_color) = match &user {
264        Some(u) if u.logged_in => (format!("Logged in as: {}", u.name), theme.accent()),
265        Some(_) => ("Not logged in".to_string(), Color::DarkGrey),
266        None => ("User context not available".to_string(), Color::Red),
267    };
268
269    View::boxed()
270        .border(true)
271        .padding(1)
272        .child(
273            View::vstack()
274                .child(
275                    View::styled_text("User Panel (reads from context)")
276                        .bold()
277                        .color(theme.primary())
278                        .build(),
279                )
280                .child(View::styled_text(status_text).color(status_color).build())
281                .build(),
282        )
283        .build()
284}
285
286// Helper function for status bar
287fn render_status_bar(cx: &Scope) -> View {
288    let theme = cx
289        .use_context::<ColorTheme>()
290        .unwrap_or(ColorTheme::Default);
291
292    View::boxed()
293        .border(true)
294        .child(
295            View::hstack()
296                .child(
297                    View::styled_text(format!(" Theme: {} ", theme.name()))
298                        .color(theme.accent())
299                        .build(),
300                )
301                .child(View::spacer())
302                .child(
303                    View::styled_text(" Context values propagate automatically ")
304                        .dim()
305                        .build(),
306                )
307                .build(),
308        )
309        .build()
310}