Skip to main content

lifecycle/
lifecycle.rs

1//! Lifecycle hooks: mount and unmount effects.
2//!
3//! Demonstrates the hooks system — components declare their effects
4//! via lifecycle(), and the framework manages them automatically.
5//! Mount fires when elements enter the tree, unmount when they leave.
6//!
7//! Run with: cargo run --example lifecycle
8
9use std::io::{self, Write};
10use std::thread;
11use std::time::Duration;
12
13use eye_declare::{Component, Elements, Hooks, InlineRenderer, Spinner, TextBlock, VStack};
14use ratatui_core::{
15    buffer::Buffer,
16    layout::Rect,
17    style::{Color, Modifier, Style},
18    text::{Line, Span},
19    widgets::Widget,
20};
21use ratatui_widgets::paragraph::Paragraph;
22
23// ---------------------------------------------------------------------------
24// A status log component that records lifecycle events.
25// `name` is a prop on the component; `entries` is internal state.
26// ---------------------------------------------------------------------------
27
28struct StatusLog {
29    name: String,
30}
31
32impl StatusLog {
33    fn new(name: impl Into<String>) -> Self {
34        Self { name: name.into() }
35    }
36}
37
38#[derive(Default)]
39struct StatusLogState {
40    entries: Vec<(String, Style)>,
41}
42
43impl StatusLogState {
44    fn log(&mut self, msg: impl Into<String>, style: Style) {
45        self.entries.push((msg.into(), style));
46    }
47}
48
49impl Component for StatusLog {
50    type State = StatusLogState;
51
52    fn render(&self, area: Rect, buf: &mut Buffer, state: &Self::State) {
53        let lines: Vec<Line> = state
54            .entries
55            .iter()
56            .map(|(text, style)| Line::from(Span::styled(text.as_str(), *style)))
57            .collect();
58        Paragraph::new(lines).render(area, buf);
59    }
60
61    fn desired_height(&self, _width: u16, state: &Self::State) -> u16 {
62        state.entries.len() as u16
63    }
64
65    fn initial_state(&self) -> Option<StatusLogState> {
66        let mut state = StatusLogState {
67            entries: Vec::new(),
68        };
69        if !self.name.is_empty() {
70            state.log(
71                format!("  {} created", self.name),
72                Style::default().fg(Color::DarkGray),
73            );
74        }
75        Some(state)
76    }
77
78    fn lifecycle(&self, hooks: &mut Hooks<StatusLogState>, _state: &StatusLogState) {
79        if !self.name.is_empty() {
80            let mount_name = self.name.clone();
81            hooks.use_mount(move |state| {
82                state.log(
83                    format!("  {} mounted", mount_name),
84                    Style::default()
85                        .fg(Color::Green)
86                        .add_modifier(Modifier::ITALIC),
87                );
88            });
89
90            let unmount_name = self.name.clone();
91            hooks.use_unmount(move |state| {
92                state.log(
93                    format!("  {} unmounted", unmount_name),
94                    Style::default()
95                        .fg(Color::Red)
96                        .add_modifier(Modifier::ITALIC),
97                );
98            });
99        }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Application state
105// ---------------------------------------------------------------------------
106
107struct AppState {
108    tasks: Vec<String>,
109    processing: bool,
110}
111
112fn task_view(state: &AppState) -> Elements {
113    let mut els = Elements::new();
114
115    els.add(
116        TextBlock::new().line(
117            format!("Tasks ({})", state.tasks.len()),
118            Style::default()
119                .fg(Color::White)
120                .add_modifier(Modifier::BOLD),
121        ),
122    );
123
124    for task in &state.tasks {
125        els.add(StatusLog::new(task)).key(task.clone());
126    }
127
128    if state.processing {
129        els.add(Spinner::new("Processing...")).key("spinner");
130    }
131
132    els.add(TextBlock::new().line("---", Style::default().fg(Color::DarkGray)));
133
134    els
135}
136
137// ---------------------------------------------------------------------------
138// Main
139// ---------------------------------------------------------------------------
140
141fn main() -> io::Result<()> {
142    let (width, _) = crossterm::terminal::size()?;
143    let mut r = InlineRenderer::new(width);
144    let mut stdout = io::stdout();
145
146    let container = r.push(VStack);
147    let mut state = AppState {
148        tasks: vec!["Alpha".into(), "Beta".into(), "Gamma".into()],
149        processing: false,
150    };
151
152    // Initial build — all three tasks mount
153    r.rebuild(container, task_view(&state));
154    flush(&mut r, &mut stdout)?;
155    thread::sleep(Duration::from_millis(1000));
156
157    // Remove "Beta" — triggers unmount for Beta, others stay
158    state.tasks.retain(|t| t != "Beta");
159    r.rebuild(container, task_view(&state));
160    flush(&mut r, &mut stdout)?;
161    thread::sleep(Duration::from_millis(1000));
162
163    // Add "Delta" — triggers mount for Delta, Alpha & Gamma get updated
164    state.tasks.push("Delta".into());
165    r.rebuild(container, task_view(&state));
166    flush(&mut r, &mut stdout)?;
167    thread::sleep(Duration::from_millis(1000));
168
169    // Start processing — spinner mounts with auto-tick
170    state.processing = true;
171    r.rebuild(container, task_view(&state));
172    // Let spinner animate
173    let start = std::time::Instant::now();
174    while start.elapsed() < Duration::from_millis(1500) && r.has_active() {
175        r.tick();
176        flush(&mut r, &mut stdout)?;
177        thread::sleep(Duration::from_millis(50));
178    }
179
180    // Clear all tasks — everything unmounts
181    state.tasks.clear();
182    state.processing = false;
183    r.rebuild(container, task_view(&state));
184    flush(&mut r, &mut stdout)?;
185
186    println!();
187    Ok(())
188}
189
190fn flush(r: &mut InlineRenderer, stdout: &mut impl Write) -> io::Result<()> {
191    let output = r.render();
192    if !output.is_empty() {
193        stdout.write_all(&output)?;
194        stdout.flush()?;
195    }
196    Ok(())
197}