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, Text, 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 initial_state(&self) -> Option<StatusLogState> {
62        let mut state = StatusLogState {
63            entries: Vec::new(),
64        };
65        if !self.name.is_empty() {
66            state.log(
67                format!("  {} created", self.name),
68                Style::default().fg(Color::DarkGray),
69            );
70        }
71        Some(state)
72    }
73
74    fn lifecycle(&self, hooks: &mut Hooks<Self, StatusLogState>, _state: &StatusLogState) {
75        if !self.name.is_empty() {
76            let mount_name = self.name.clone();
77            hooks.use_mount(move |_props, state| {
78                state.log(
79                    format!("  {} mounted", mount_name),
80                    Style::default()
81                        .fg(Color::Green)
82                        .add_modifier(Modifier::ITALIC),
83                );
84            });
85
86            let unmount_name = self.name.clone();
87            hooks.use_unmount(move |_props, state| {
88                state.log(
89                    format!("  {} unmounted", unmount_name),
90                    Style::default()
91                        .fg(Color::Red)
92                        .add_modifier(Modifier::ITALIC),
93                );
94            });
95        }
96    }
97}
98
99// ---------------------------------------------------------------------------
100// Application state
101// ---------------------------------------------------------------------------
102
103struct AppState {
104    tasks: Vec<String>,
105    processing: bool,
106}
107
108fn task_view(state: &AppState) -> Elements {
109    let mut els = Elements::new();
110
111    els.add(Text::styled(
112        format!("Tasks ({})", state.tasks.len()),
113        Style::default()
114            .fg(Color::White)
115            .add_modifier(Modifier::BOLD),
116    ));
117
118    for task in &state.tasks {
119        els.add(StatusLog::new(task)).key(task.clone());
120    }
121
122    if state.processing {
123        els.add(Spinner::new("Processing...")).key("spinner");
124    }
125
126    els.add(Text::styled("---", Style::default().fg(Color::DarkGray)));
127
128    els
129}
130
131// ---------------------------------------------------------------------------
132// Main
133// ---------------------------------------------------------------------------
134
135fn main() -> io::Result<()> {
136    let (width, _) = crossterm::terminal::size()?;
137    let mut r = InlineRenderer::new(width);
138    let mut stdout = io::stdout();
139
140    let container = r.push(VStack);
141    let mut state = AppState {
142        tasks: vec!["Alpha".into(), "Beta".into(), "Gamma".into()],
143        processing: false,
144    };
145
146    // Initial build — all three tasks mount
147    r.rebuild(container, task_view(&state));
148    flush(&mut r, &mut stdout)?;
149    thread::sleep(Duration::from_millis(1000));
150
151    // Remove "Beta" — triggers unmount for Beta, others stay
152    state.tasks.retain(|t| t != "Beta");
153    r.rebuild(container, task_view(&state));
154    flush(&mut r, &mut stdout)?;
155    thread::sleep(Duration::from_millis(1000));
156
157    // Add "Delta" — triggers mount for Delta, Alpha & Gamma get updated
158    state.tasks.push("Delta".into());
159    r.rebuild(container, task_view(&state));
160    flush(&mut r, &mut stdout)?;
161    thread::sleep(Duration::from_millis(1000));
162
163    // Start processing — spinner mounts with auto-tick
164    state.processing = true;
165    r.rebuild(container, task_view(&state));
166    // Let spinner animate
167    let start = std::time::Instant::now();
168    while start.elapsed() < Duration::from_millis(1500) && r.has_active() {
169        r.tick();
170        flush(&mut r, &mut stdout)?;
171        thread::sleep(Duration::from_millis(50));
172    }
173
174    // Clear all tasks — everything unmounts
175    state.tasks.clear();
176    state.processing = false;
177    r.rebuild(container, task_view(&state));
178    flush(&mut r, &mut stdout)?;
179
180    println!();
181    Ok(())
182}
183
184fn flush(r: &mut InlineRenderer, stdout: &mut impl Write) -> io::Result<()> {
185    let output = r.render();
186    if !output.is_empty() {
187        stdout.write_all(&output)?;
188        stdout.flush()?;
189    }
190    Ok(())
191}