38_custom_widget/
38_custom_widget.rs1use 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 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; }
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, (false, 3) => true, _ => false, };
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 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}