Skip to main content

input_demo/
input_demo.rs

1//! Input Demo
2//!
3//! This example demonstrates the keyboard and mouse input capabilities of MinUI,
4//! displaying events in a structured panel on the screen.
5
6use minui::MouseButton;
7use minui::input::ClickTracker;
8use minui::prelude::*;
9use std::collections::VecDeque;
10use std::time::Duration;
11
12// NOTE: `TextBlock` currently doesn't treat `\n` as hard line breaks in its wrapping logic
13// (it wraps based on whitespace). For input demo, we want one event per line.
14// Until `TextBlock` becomes newline-aware, render log as a vertical `Container` of `Label`s.
15const MAX_EVENTS: usize = 12;
16
17struct InputDemoState {
18    event_log: VecDeque<String>,
19    mouse_pos: (u16, u16),
20    click_tracker: ClickTracker,
21}
22
23fn main() -> minui::Result<()> {
24    let initial_state = InputDemoState {
25        event_log: {
26            let mut log = VecDeque::new();
27            log.push_back("Welcome to MinUI Input Demo!".to_string());
28            log.push_back("Try typing, moving mouse, clicking...".to_string());
29            log.push_back("Double-click quickly to see double-click detection!".to_string());
30            log.push_back("Press 'q' to quit".to_string());
31            log
32        },
33        mouse_pos: (0, 0),
34        click_tracker: ClickTracker::new(),
35    };
36
37    let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(16));
38
39    app.run(
40        |state, event| {
41            // Return false to exit (supports modifier-aware and legacy events).
42            if let Event::KeyWithModifiers(k) = event {
43                if matches!(k.key, KeyKind::Char('q')) {
44                    return false;
45                }
46            }
47            if matches!(event, Event::Character('q')) {
48                return false;
49            }
50
51            // Handle events and update state
52            match event {
53                // Prefer modifier-aware keyboard events (the keyboard handler may emit these for most keys).
54                Event::KeyWithModifiers(k) => match k.key {
55                    KeyKind::Char(c) => {
56                        state.event_log.push_back(format!(
57                            "Key: '{}' (mods: shift={}, ctrl={}, alt={}, super={})",
58                            c, k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
59                        ));
60                    }
61                    KeyKind::Up => {
62                        state.event_log.push_back(format!(
63                            "Key: ↑ Up (mods: shift={}, ctrl={}, alt={}, super={})",
64                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
65                        ));
66                    }
67                    KeyKind::Down => {
68                        state.event_log.push_back(format!(
69                            "Key: ↓ Down (mods: shift={}, ctrl={}, alt={}, super={})",
70                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
71                        ));
72                    }
73                    KeyKind::Left => {
74                        state.event_log.push_back(format!(
75                            "Key: ← Left (mods: shift={}, ctrl={}, alt={}, super={})",
76                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
77                        ));
78                    }
79                    KeyKind::Right => {
80                        state.event_log.push_back(format!(
81                            "Key: → Right (mods: shift={}, ctrl={}, alt={}, super={})",
82                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
83                        ));
84                    }
85                    KeyKind::Enter => {
86                        state.event_log.push_back(format!(
87                            "Key: ⏎ Enter (mods: shift={}, ctrl={}, alt={}, super={})",
88                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
89                        ));
90                    }
91                    KeyKind::Escape => {
92                        state.event_log.push_back(format!(
93                            "Key: Escape (mods: shift={}, ctrl={}, alt={}, super={})",
94                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
95                        ));
96                    }
97                    KeyKind::Backspace => {
98                        state.event_log.push_back(format!(
99                            "Key: ⌫ Backspace (mods: shift={}, ctrl={}, alt={}, super={})",
100                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
101                        ));
102                    }
103                    KeyKind::Delete => {
104                        state.event_log.push_back(format!(
105                            "Key: ⌦ Delete (mods: shift={}, ctrl={}, alt={}, super={})",
106                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
107                        ));
108                    }
109                    KeyKind::Tab => {
110                        state.event_log.push_back(format!(
111                            "Key: Tab (mods: shift={}, ctrl={}, alt={}, super={})",
112                            k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
113                        ));
114                    }
115                    KeyKind::Function(n) => {
116                        state.event_log.push_back(format!(
117                            "Key: F{} (mods: shift={}, ctrl={}, alt={}, super={})",
118                            n, k.mods.shift, k.mods.ctrl, k.mods.alt, k.mods.super_key
119                        ));
120                    }
121                    KeyKind::CapsLock => todo!(),
122                },
123
124                // Legacy fallback keyboard events (some backends may still emit these).
125                Event::Character(c) => {
126                    state.event_log.push_back(format!("Key: '{}'", c));
127                }
128                Event::Paste(text) => {
129                    // Keep the log readable for large pastes
130                    let preview: String = text.chars().take(60).collect();
131                    if text.chars().count() > 60 {
132                        state.event_log.push_back(format!(
133                            "Paste: \"{}…\" ({} chars)",
134                            preview,
135                            text.chars().count()
136                        ));
137                    } else {
138                        state.event_log.push_back(format!("Paste: \"{}\"", preview));
139                    }
140                }
141                Event::KeyUp => {
142                    state.event_log.push_back("Key: ↑ Up".to_string());
143                }
144                Event::KeyDown => {
145                    state.event_log.push_back("Key: ↓ Down".to_string());
146                }
147                Event::KeyLeft => {
148                    state.event_log.push_back("Key: ← Left".to_string());
149                }
150                Event::KeyRight => {
151                    state.event_log.push_back("Key: → Right".to_string());
152                }
153                Event::Enter => {
154                    state.event_log.push_back("Key: ⏎ Enter".to_string());
155                }
156                Event::Escape => {
157                    state.event_log.push_back("Key: Escape".to_string());
158                }
159                Event::Backspace => {
160                    state.event_log.push_back("Key: ⌫ Backspace".to_string());
161                }
162                Event::Delete => {
163                    state.event_log.push_back("Key: ⌦ Delete".to_string());
164                }
165                Event::FunctionKey(n) => {
166                    state.event_log.push_back(format!("Key: F{}", n));
167                }
168
169                // Handle mouse events
170                Event::MouseMove { x, y } => {
171                    state.mouse_pos = (x, y);
172                    // Only log occasional moves to avoid spam
173                    if x % 3 == 0 && y % 3 == 0 {
174                        state
175                            .event_log
176                            .push_back(format!("Mouse: Moved to ({}, {})", x, y));
177                    }
178                }
179                Event::MouseClick { x, y, button } => {
180                    state.mouse_pos = (x, y);
181                    let button_name = match button {
182                        MouseButton::Left => "Left",
183                        MouseButton::Right => "Right",
184                        MouseButton::Middle => "Middle",
185                        MouseButton::Other(_) => "Other",
186                    };
187
188                    // Check for double-click
189                    if state.click_tracker.is_double_click(x, y) {
190                        state.event_log.push_back(format!(
191                            "Mouse: DOUBLE-CLICK! {} button at ({}, {})",
192                            button_name, x, y
193                        ));
194                    } else {
195                        state
196                            .event_log
197                            .push_back(format!("Mouse: {} click at ({}, {})", button_name, x, y));
198                    }
199                }
200                Event::MouseDrag { x, y, button } => {
201                    state.mouse_pos = (x, y);
202                    let button_name = match button {
203                        MouseButton::Left => "Left",
204                        MouseButton::Right => "Right",
205                        MouseButton::Middle => "Middle",
206                        MouseButton::Other(_) => "Other",
207                    };
208                    state
209                        .event_log
210                        .push_back(format!("Mouse: {} drag to ({}, {})", button_name, x, y));
211                }
212                Event::MouseScroll { delta } => {
213                    let direction = if delta > 0 { "up" } else { "down" };
214                    state
215                        .event_log
216                        .push_back(format!("Mouse: Scroll {} ({})", direction, delta));
217                }
218                Event::MouseScrollHorizontal { delta } => {
219                    let direction = if delta > 0 { "right" } else { "left" };
220                    state
221                        .event_log
222                        .push_back(format!("Mouse: Scroll {} ({})", direction, delta));
223                }
224                Event::MouseRelease { x, y, button } => {
225                    state.mouse_pos = (x, y);
226                    let button_name = match button {
227                        MouseButton::Left => "Left",
228                        MouseButton::Right => "Right",
229                        MouseButton::Middle => "Middle",
230                        MouseButton::Other(_) => "Other",
231                    };
232                    state
233                        .event_log
234                        .push_back(format!("Mouse: {} release at ({}, {})", button_name, x, y));
235                }
236
237                Event::Resize { width, height } => {
238                    state
239                        .event_log
240                        .push_back(format!("Terminal: Resized to {}x{}", width, height));
241                }
242                _ => {}
243            }
244
245            // Keep the log at reasonable size
246            if state.event_log.len() > MAX_EVENTS {
247                state.event_log.pop_front();
248            }
249
250            true
251        },
252        |state, window| {
253            let (term_width, term_height) = window.get_size();
254
255            // Create a container to display the events.
256            //
257            // Panel has been absorbed into Container: use borders + title + padding, and put
258            // content widgets inside as children.
259            // NOTE: `ContainerPadding` is the name exported by the prelude for Container's padding type.
260            // (The underlying type in `container.rs` is `Padding`.)
261            use minui::widgets::ContainerPadding;
262
263            let panel_x: u16 = 2u16;
264            let panel_y: u16 = 1u16;
265            let panel_w: u16 = term_width.saturating_sub(4u16);
266            let panel_h: u16 = term_height.saturating_sub(4u16);
267
268            // Render the log as stacked labels so each event appears on its own line.
269            // We display the newest entries at the top (reverse chronological).
270            let mut log_container = Container::vertical().with_row_gap(Gap::Pixels(0u16));
271            if state.event_log.is_empty() {
272                log_container = log_container.add_child(Label::new("No events yet..."));
273            } else {
274                for line in state.event_log.iter().rev().take(MAX_EVENTS) {
275                    log_container = log_container.add_child(Label::new(line.clone()));
276                }
277            }
278
279            let panel = Container::new()
280                .with_position_and_size(panel_x, panel_y, panel_w, panel_h)
281                .with_border()
282                .with_border_chars(BorderChars::double_line())
283                .with_border_color(ColorPair::new(Color::Cyan, Color::Black))
284                .with_title("MinUI Input Demo")
285                .with_title_alignment(TitleAlignment::Center)
286                .with_padding(ContainerPadding::uniform(1u16))
287                .add_child(log_container);
288
289            panel.draw(window)?;
290
291            // Draw mouse position info at bottom
292            let mouse_info = format!("Mouse: ({}, {})", state.mouse_pos.0, state.mouse_pos.1);
293            let info_y = term_height.saturating_sub(2);
294            window.write_str_colored(
295                info_y,
296                2,
297                &mouse_info,
298                ColorPair::new(Color::Cyan, Color::Transparent),
299            )?;
300
301            // Draw instructions at the very bottom
302            let help_text = "Press 'q' to quit | Try typing, clicking, scrolling!";
303            let help_x = (term_width.saturating_sub(help_text.len() as u16)) / 2;
304            let help_y = term_height.saturating_sub(1);
305            window.write_str_colored(
306                help_y,
307                help_x,
308                help_text,
309                ColorPair::new(Color::DarkGray, Color::Transparent),
310            )?;
311
312            window.flush()?;
313            Ok(())
314        },
315    )?;
316
317    Ok(())
318}