Skip to main content

38_custom_widget/
38_custom_widget.rs

1//! Example 38: Custom Widget — Game of Life
2//!
3//! Conway's Game of Life implemented as a custom Widget.
4//! Demonstrates the Widget trait escape hatch for character-cell rendering.
5//!
6//! Run with: `cargo run -p telex-tui --example 38_custom_widget`
7
8use crossterm::event::KeyCode;
9use crossterm::style::Color;
10use std::cell::RefCell;
11use std::rc::Rc;
12use std::time::Duration;
13use telex::buffer::{Buffer, Rect};
14use telex::prelude::*;
15use telex::widget::Widget;
16
17telex::require_api!(0, 2);
18
19fn main() {
20    telex::run(App).unwrap();
21}
22
23const GRID_W: usize = 40;
24const GRID_H: usize = 20;
25
26#[derive(Clone)]
27struct GameOfLife {
28    grid: Vec<Vec<bool>>,
29}
30
31impl GameOfLife {
32    fn new() -> Self {
33        Self {
34            grid: vec![vec![false; GRID_W]; GRID_H],
35        }
36    }
37
38    fn randomize(&mut self) {
39        // Simple pseudo-random using a seed based on grid state
40        let mut seed: u64 = 12345;
41        for row in &mut self.grid {
42            for cell in row.iter_mut() {
43                seed ^= seed << 13;
44                seed ^= seed >> 7;
45                seed ^= seed << 17;
46                *cell = seed % 3 == 0; // ~33% alive
47            }
48        }
49    }
50
51    fn clear(&mut self) {
52        for row in &mut self.grid {
53            for cell in row.iter_mut() {
54                *cell = false;
55            }
56        }
57    }
58
59    fn step(&mut self) {
60        let mut next = vec![vec![false; GRID_W]; GRID_H];
61        for y in 0..GRID_H {
62            for x in 0..GRID_W {
63                let neighbors = self.count_neighbors(x, y);
64                next[y][x] = match (self.grid[y][x], neighbors) {
65                    (true, 2..=3) => true,  // survive
66                    (false, 3) => true,     // birth
67                    _ => false,             // die
68                };
69            }
70        }
71        self.grid = next;
72    }
73
74    fn count_neighbors(&self, x: usize, y: usize) -> u8 {
75        let mut count = 0u8;
76        for dy in [-1i32, 0, 1] {
77            for dx in [-1i32, 0, 1] {
78                if dy == 0 && dx == 0 {
79                    continue;
80                }
81                let nx = (x as i32 + dx).rem_euclid(GRID_W as i32) as usize;
82                let ny = (y as i32 + dy).rem_euclid(GRID_H as i32) as usize;
83                if self.grid[ny][nx] {
84                    count += 1;
85                }
86            }
87        }
88        count
89    }
90
91    fn alive_count(&self) -> usize {
92        self.grid.iter().flat_map(|r| r.iter()).filter(|&&c| c).count()
93    }
94}
95
96impl Widget for GameOfLife {
97    fn render(&self, area: Rect, buf: &mut Buffer) {
98        for y in 0..GRID_H.min(area.height as usize) {
99            for x in 0..GRID_W.min(area.width as usize) {
100                let ch = if self.grid[y][x] { '\u{2588}' } else { '\u{00B7}' };
101                let fg = if self.grid[y][x] { Color::Green } else { Color::DarkGrey };
102                buf.set(area.x + x as u16, area.y + y as u16, ch, fg, Color::Reset);
103            }
104        }
105    }
106
107    fn height_hint(&self, _width: u16) -> Option<u16> {
108        Some(GRID_H as u16)
109    }
110
111    fn width_hint(&self) -> Option<u16> {
112        Some(GRID_W as u16)
113    }
114}
115
116struct App;
117
118impl Component for App {
119    fn render(&self, cx: Scope) -> View {
120        let show_help = state!(cx, || false);
121
122        cx.use_command(
123            KeyBinding::key(KeyCode::F(1)),
124            with!(show_help => move || show_help.update(|v| *v = !*v)),
125        );
126
127        let game: State<GameOfLife> = state!(cx, || {
128            let mut g = GameOfLife::new();
129            g.randomize();
130            g
131        });
132        let generation = state!(cx, || 0u64);
133        let playing = state!(cx, || false);
134
135        // Auto-step when playing
136        interval!(cx, Duration::from_millis(150), with!(playing, game, generation => move || {
137            if playing.get() {
138                game.update(|g| g.step());
139                generation.update(|n| *n += 1);
140            }
141        }));
142
143        let widget = Rc::new(RefCell::new(game.get()));
144
145        View::vstack()
146            .spacing(1)
147            .child(View::styled_text("Game of Life").bold().build())
148            .child(
149                View::hstack()
150                    .spacing(1)
151                    .child(View::styled_text(format!("Gen: {}", generation.get())).dim().build())
152                    .child(View::styled_text(format!("Alive: {}", game.get().alive_count())).dim().build())
153                    .child(if playing.get() {
154                        View::styled_text("PLAYING").color(Color::Green).bold().build()
155                    } else {
156                        View::styled_text("PAUSED").color(Color::Yellow).build()
157                    })
158                    .build(),
159            )
160            .child(View::custom(widget))
161            .child(
162                View::hstack()
163                    .spacing(1)
164                    .child(
165                        View::button()
166                            .label("[ Step ]")
167                            .on_press(with!(game, generation => move || {
168                                game.update(|g| g.step());
169                                generation.update(|n| *n += 1);
170                            }))
171                            .build(),
172                    )
173                    .child(
174                        View::button()
175                            .label(if playing.get() { "[ Pause ]" } else { "[ Play ]" })
176                            .on_press(with!(playing => move || playing.update(|p| *p = !*p)))
177                            .build(),
178                    )
179                    .child(
180                        View::button()
181                            .label("[ Randomize ]")
182                            .on_press(with!(game, generation => move || {
183                                game.update(|g| g.randomize());
184                                generation.set(0);
185                            }))
186                            .build(),
187                    )
188                    .child(
189                        View::button()
190                            .label("[ Clear ]")
191                            .on_press(with!(game, generation => move || {
192                                game.update(|g| g.clear());
193                                generation.set(0);
194                            }))
195                            .build(),
196                    )
197                    .build(),
198            )
199            .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
200            .child(
201                View::modal()
202                    .visible(show_help.get())
203                    .title("Example 38: Custom Widget")
204                    .on_dismiss(with!(show_help => move || show_help.set(false)))
205                    .child(
206                        View::vstack()
207                            .child(View::styled_text("What you're seeing").bold().build())
208                            .child(View::text("• Conway's Game of Life"))
209                            .child(View::text("• Custom Widget renders the grid"))
210                            .child(View::text("• interval! drives auto-play"))
211                            .child(View::gap(1))
212                            .child(View::styled_text("Key concepts").bold().build())
213                            .child(View::text("• impl Widget for YourStruct"))
214                            .child(View::text("• render(area, buf) draws cells"))
215                            .child(View::text("• height_hint / width_hint for sizing"))
216                            .child(View::text("• View::custom(Rc<RefCell<W>>)"))
217                            .child(View::gap(1))
218                            .child(View::styled_text("Try this").bold().build())
219                            .child(View::text("• Press Play to auto-step"))
220                            .child(View::text("• Step manually one at a time"))
221                            .child(View::text("• Randomize for a new pattern"))
222                            .child(View::text("• Clear then Step to watch"))
223                            .child(View::gap(1))
224                            .child(View::styled_text("Next up").bold().build())
225                            .child(View::text("-> 39_port: bidirectional comms"))
226                            .child(View::gap(1))
227                            .child(View::styled_text("Press Escape to close").dim().build())
228                            .build(),
229                    )
230                    .build(),
231            )
232            .build()
233    }
234}