1pub mod anim;
38pub mod buffer;
39pub mod cell;
40pub mod context;
41pub mod event;
42pub mod layout;
43pub mod rect;
44pub mod style;
45mod terminal;
46pub mod widgets;
47
48use std::io;
49use std::time::Duration;
50
51use event::Event;
52use terminal::{InlineTerminal, Terminal};
53
54pub use anim::{Spring, Tween};
55pub use context::{Context, Response};
56pub use event::{KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseKind};
57pub use style::{Align, Border, Color, Constraints, Margin, Modifiers, Padding, Style, Theme};
58pub use widgets::{
59 ListState, ScrollState, SpinnerState, TableState, TabsState, TextInputState, TextareaState,
60 ToastLevel, ToastMessage, ToastState,
61};
62
63pub struct RunConfig {
81 pub tick_rate: Duration,
86 pub mouse: bool,
91 pub theme: Theme,
95}
96
97impl Default for RunConfig {
98 fn default() -> Self {
99 Self {
100 tick_rate: Duration::from_millis(100),
101 mouse: false,
102 theme: Theme::dark(),
103 }
104 }
105}
106
107pub fn run(f: impl FnMut(&mut Context)) -> io::Result<()> {
122 run_with(RunConfig::default(), f)
123}
124
125pub fn run_with(config: RunConfig, mut f: impl FnMut(&mut Context)) -> io::Result<()> {
145 let mut term = Terminal::new(config.mouse)?;
146 let mut events: Vec<Event> = Vec::new();
147 let mut debug_mode: bool = false;
148 let mut tick: u64 = 0;
149 let mut focus_index: usize = 0;
150 let mut prev_focus_count: usize = 0;
151 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
152 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
153 let mut last_mouse_pos: Option<(u32, u32)> = None;
154
155 loop {
156 let (w, h) = term.size();
157 let mut ctx = Context::new(
158 events.clone(),
159 w,
160 h,
161 tick,
162 focus_index,
163 prev_focus_count,
164 std::mem::take(&mut prev_scroll_infos),
165 std::mem::take(&mut prev_hit_map),
166 debug_mode,
167 config.theme,
168 last_mouse_pos,
169 );
170 ctx.process_focus_keys();
171
172 f(&mut ctx);
173
174 if ctx.should_quit {
175 break;
176 }
177
178 focus_index = ctx.focus_index;
179 prev_focus_count = ctx.focus_count;
180
181 let mut tree = layout::build_tree(&ctx.commands);
182 let area = crate::rect::Rect::new(0, 0, w, h);
183 layout::compute(&mut tree, area);
184 prev_scroll_infos = layout::collect_scroll_infos(&tree);
185 prev_hit_map = layout::collect_hit_areas(&tree);
186 layout::render(&tree, term.buffer_mut());
187 if debug_mode {
188 layout::render_debug_overlay(&tree, term.buffer_mut());
189 }
190
191 term.flush()?;
192 tick = tick.wrapping_add(1);
193
194 events.clear();
195 if crossterm::event::poll(config.tick_rate)? {
196 let raw = crossterm::event::read()?;
197 if let Some(ev) = event::from_crossterm(raw) {
198 if is_ctrl_c(&ev) {
199 break;
200 }
201 if let Event::Resize(_, _) = &ev {
202 term.handle_resize()?;
203 }
204 events.push(ev);
205 }
206
207 while crossterm::event::poll(Duration::ZERO)? {
208 let raw = crossterm::event::read()?;
209 if let Some(ev) = event::from_crossterm(raw) {
210 if is_ctrl_c(&ev) {
211 return Ok(());
212 }
213 if let Event::Resize(_, _) = &ev {
214 term.handle_resize()?;
215 }
216 events.push(ev);
217 }
218 }
219
220 for ev in &events {
221 if matches!(
222 ev,
223 Event::Key(event::KeyEvent {
224 code: KeyCode::F(12),
225 ..
226 })
227 ) {
228 debug_mode = !debug_mode;
229 }
230 }
231 }
232
233 for ev in &events {
234 if let Event::Mouse(mouse) = ev {
235 last_mouse_pos = Some((mouse.x, mouse.y));
236 }
237 }
238
239 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
240 prev_hit_map.clear();
241 prev_scroll_infos.clear();
242 last_mouse_pos = None;
243 }
244 }
245
246 Ok(())
247}
248
249#[cfg(feature = "async")]
270pub fn run_async<M: Send + 'static>(
271 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
272) -> io::Result<tokio::sync::mpsc::Sender<M>> {
273 run_async_with(RunConfig::default(), f)
274}
275
276#[cfg(feature = "async")]
283pub fn run_async_with<M: Send + 'static>(
284 config: RunConfig,
285 f: impl FnMut(&mut Context, &mut Vec<M>) + Send + 'static,
286) -> io::Result<tokio::sync::mpsc::Sender<M>> {
287 let (tx, rx) = tokio::sync::mpsc::channel(100);
288 let handle =
289 tokio::runtime::Handle::try_current().map_err(|err| io::Error::other(err.to_string()))?;
290
291 handle.spawn_blocking(move || {
292 let _ = run_async_loop(config, f, rx);
293 });
294
295 Ok(tx)
296}
297
298#[cfg(feature = "async")]
299fn run_async_loop<M: Send + 'static>(
300 config: RunConfig,
301 mut f: impl FnMut(&mut Context, &mut Vec<M>) + Send,
302 mut rx: tokio::sync::mpsc::Receiver<M>,
303) -> io::Result<()> {
304 let mut term = Terminal::new(config.mouse)?;
305 let mut events: Vec<Event> = Vec::new();
306 let mut tick: u64 = 0;
307 let mut focus_index: usize = 0;
308 let mut prev_focus_count: usize = 0;
309 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
310 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
311 let mut last_mouse_pos: Option<(u32, u32)> = None;
312
313 loop {
314 let mut messages: Vec<M> = Vec::new();
315 while let Ok(message) = rx.try_recv() {
316 messages.push(message);
317 }
318
319 let (w, h) = term.size();
320 let mut ctx = Context::new(
321 events.clone(),
322 w,
323 h,
324 tick,
325 focus_index,
326 prev_focus_count,
327 std::mem::take(&mut prev_scroll_infos),
328 std::mem::take(&mut prev_hit_map),
329 false,
330 config.theme,
331 last_mouse_pos,
332 );
333 ctx.process_focus_keys();
334
335 f(&mut ctx, &mut messages);
336
337 if ctx.should_quit {
338 break;
339 }
340
341 focus_index = ctx.focus_index;
342 prev_focus_count = ctx.focus_count;
343
344 let mut tree = layout::build_tree(&ctx.commands);
345 let area = crate::rect::Rect::new(0, 0, w, h);
346 layout::compute(&mut tree, area);
347 prev_scroll_infos = layout::collect_scroll_infos(&tree);
348 prev_hit_map = layout::collect_hit_areas(&tree);
349 layout::render(&tree, term.buffer_mut());
350
351 term.flush()?;
352 tick = tick.wrapping_add(1);
353
354 events.clear();
355 if crossterm::event::poll(config.tick_rate)? {
356 let raw = crossterm::event::read()?;
357 if let Some(ev) = event::from_crossterm(raw) {
358 if is_ctrl_c(&ev) {
359 break;
360 }
361 if let Event::Resize(_, _) = &ev {
362 term.handle_resize()?;
363 prev_hit_map.clear();
364 prev_scroll_infos.clear();
365 last_mouse_pos = None;
366 }
367 events.push(ev);
368 }
369
370 while crossterm::event::poll(Duration::ZERO)? {
371 let raw = crossterm::event::read()?;
372 if let Some(ev) = event::from_crossterm(raw) {
373 if is_ctrl_c(&ev) {
374 return Ok(());
375 }
376 if let Event::Resize(_, _) = &ev {
377 term.handle_resize()?;
378 prev_hit_map.clear();
379 prev_scroll_infos.clear();
380 last_mouse_pos = None;
381 }
382 events.push(ev);
383 }
384 }
385 }
386
387 for ev in &events {
388 if let Event::Mouse(mouse) = ev {
389 last_mouse_pos = Some((mouse.x, mouse.y));
390 }
391 }
392 }
393
394 Ok(())
395}
396
397pub fn run_inline(height: u32, f: impl FnMut(&mut Context)) -> io::Result<()> {
413 run_inline_with(height, RunConfig::default(), f)
414}
415
416pub fn run_inline_with(
421 height: u32,
422 config: RunConfig,
423 mut f: impl FnMut(&mut Context),
424) -> io::Result<()> {
425 let mut term = InlineTerminal::new(height, config.mouse)?;
426 let mut events: Vec<Event> = Vec::new();
427 let mut debug_mode: bool = false;
428 let mut tick: u64 = 0;
429 let mut focus_index: usize = 0;
430 let mut prev_focus_count: usize = 0;
431 let mut prev_scroll_infos: Vec<(u32, u32)> = Vec::new();
432 let mut prev_hit_map: Vec<rect::Rect> = Vec::new();
433 let mut last_mouse_pos: Option<(u32, u32)> = None;
434
435 loop {
436 let (w, h) = term.size();
437 let mut ctx = Context::new(
438 events.clone(),
439 w,
440 h,
441 tick,
442 focus_index,
443 prev_focus_count,
444 std::mem::take(&mut prev_scroll_infos),
445 std::mem::take(&mut prev_hit_map),
446 debug_mode,
447 config.theme,
448 last_mouse_pos,
449 );
450 ctx.process_focus_keys();
451
452 f(&mut ctx);
453
454 if ctx.should_quit {
455 break;
456 }
457
458 focus_index = ctx.focus_index;
459 prev_focus_count = ctx.focus_count;
460
461 let mut tree = layout::build_tree(&ctx.commands);
462 let area = crate::rect::Rect::new(0, 0, w, h);
463 layout::compute(&mut tree, area);
464 prev_scroll_infos = layout::collect_scroll_infos(&tree);
465 prev_hit_map = layout::collect_hit_areas(&tree);
466 layout::render(&tree, term.buffer_mut());
467 if debug_mode {
468 layout::render_debug_overlay(&tree, term.buffer_mut());
469 }
470
471 term.flush()?;
472 tick = tick.wrapping_add(1);
473
474 events.clear();
475 if crossterm::event::poll(config.tick_rate)? {
476 let raw = crossterm::event::read()?;
477 if let Some(ev) = event::from_crossterm(raw) {
478 if is_ctrl_c(&ev) {
479 break;
480 }
481 if let Event::Resize(_, _) = &ev {
482 term.handle_resize()?;
483 }
484 events.push(ev);
485 }
486
487 while crossterm::event::poll(Duration::ZERO)? {
488 let raw = crossterm::event::read()?;
489 if let Some(ev) = event::from_crossterm(raw) {
490 if is_ctrl_c(&ev) {
491 return Ok(());
492 }
493 if let Event::Resize(_, _) = &ev {
494 term.handle_resize()?;
495 }
496 events.push(ev);
497 }
498 }
499
500 for ev in &events {
501 if matches!(
502 ev,
503 Event::Key(event::KeyEvent {
504 code: KeyCode::F(12),
505 ..
506 })
507 ) {
508 debug_mode = !debug_mode;
509 }
510 }
511 }
512
513 for ev in &events {
514 if let Event::Mouse(mouse) = ev {
515 last_mouse_pos = Some((mouse.x, mouse.y));
516 }
517 }
518
519 if events.iter().any(|e| matches!(e, Event::Resize(_, _))) {
520 prev_hit_map.clear();
521 prev_scroll_infos.clear();
522 last_mouse_pos = None;
523 }
524 }
525
526 Ok(())
527}
528
529fn is_ctrl_c(ev: &Event) -> bool {
530 matches!(
531 ev,
532 Event::Key(event::KeyEvent {
533 code: KeyCode::Char('c'),
534 modifiers,
535 }) if modifiers.contains(KeyModifiers::CONTROL)
536 )
537}