pub struct App<S> { /* private fields */ }Expand description
Application runner that manages the main loop.
Handles window setup, input polling, and timing so you can focus on your application logic. Can run in event-driven mode (default) or with a fixed frame rate for animated applications (and possibly games in the future).
Implementations§
Source§impl<S> App<S>
impl<S> App<S>
Sourcepub fn new(initial_state: S) -> Result<Self>
pub fn new(initial_state: S) -> Result<Self>
Creates a new app with the given initial state.
Examples found in repository?
18fn main() -> minui::Result<()> {
19 let mut app = App::new(())?;
20
21 app.run(
22 |_state, event| {
23 // Quit on 'q' (supports both modifier-aware and legacy events).
24 match event {
25 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
26 Event::Character('q') => false,
27 _ => true,
28 }
29 },
30 |_state, window| {
31 let (w, h) = window.get_size();
32 create_app_layout(w, h).draw(window)?;
33 window.flush()?;
34 Ok(())
35 },
36 )?;
37
38 Ok(())
39}More examples
36fn main() -> minui::Result<()> {
37 let mut app = App::new(())?;
38
39 app.run(
40 |_state, event| {
41 // Return false to exit.
42 //
43 // The keyboard handler may emit:
44 // - `Event::KeyWithModifiers(KeyKind::Char('q'))` (modifier-aware), or
45 // - `Event::Character('q')` (legacy fallback).
46 match event {
47 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
48 Event::Character('q') => false,
49 _ => true,
50 }
51 },
52 |_state, window| {
53 let (width, height) = window.get_size();
54
55 // Title
56 window.write_str(0, 0, "RGB Color & Gradient Demo (press 'q' to quit)")?;
57
58 // --- All drawing calls happen here ---
59 draw_rainbow_gradient(window, 2, width)?;
60 draw_color_themes(window, 6)?;
61 draw_vertical_gradient(window, 12, width, height)?;
62 draw_color_blocks(window, height)?;
63
64 window.end_frame()?;
65 Ok(())
66 },
67 )?;
68
69 Ok(())
70}19fn main() -> minui::Result<()> {
20 let profile_store = Arc::new(Mutex::new(None));
21
22 let mut app = App::new(DemoState {
23 frame_count: 0,
24 latest_profile: Arc::clone(&profile_store),
25 })?
26 .with_frame_rate(Duration::from_millis(16))
27 .with_frame_budget(Duration::from_millis(16));
28
29 {
30 let store = Arc::clone(&profile_store);
31 app.set_frame_profile_hook(move |profile| {
32 if let Ok(mut slot) = store.lock() {
33 *slot = Some(*profile);
34 }
35 });
36 }
37
38 app.run(
39 |state, event| {
40 match event {
41 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => return false,
42 Event::Character('q') => return false,
43 Event::Frame => {
44 state.frame_count = state.frame_count.saturating_add(1);
45 }
46 _ => {}
47 }
48 true
49 },
50 |state, window| {
51 draw_header(window)?;
52 draw_code(window)?;
53 draw_profile_footer(state, window)?;
54 window.flush()?;
55 Ok(())
56 },
57 )
58}22fn main() -> minui::Result<()> {
23 // Start with player at position (5, 5)
24 let initial_state = MyCoolApp { x: 5, y: 5 };
25
26 // Enable a fixed frame rate (timed updates) every 100ms
27 let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(100));
28
29 // Run the main loop with update and draw functions
30 app.run(
31 // Update function: handle events and modify state
32 // Return false to exit, true to continue
33 |state, event| {
34 match event {
35 // Prefer modifier-aware key events (the keyboard handler may emit these for most keys).
36 Event::KeyWithModifiers(k) => match k.key {
37 KeyKind::Char('q') => return false,
38 KeyKind::Up => state.y = state.y.saturating_sub(1),
39 KeyKind::Down => state.y += 1,
40 KeyKind::Left => state.x = state.x.saturating_sub(1),
41 KeyKind::Right => state.x += 1,
42 _ => {}
43 },
44
45 // Legacy fallback.
46 Event::Character('q') => return false,
47
48 Event::KeyUp => state.y = state.y.saturating_sub(1),
49 Event::KeyDown => state.y += 1,
50 Event::KeyLeft => state.x = state.x.saturating_sub(1),
51 Event::KeyRight => state.x += 1,
52
53 Event::Frame => {
54 // Automatic movement every frame event (100ms)
55 state.x += 1;
56 }
57 _ => {}
58 }
59 // Keep running
60 true
61 },
62 // Draw function: render the current state
63 |state, window| {
64 window.write_str(state.y, state.x, "@")?;
65 window.write_str(0, 0, "Press 'q' to quit")?;
66 window.flush()?;
67 Ok(())
68 },
69 )?;
70
71 Ok(())
72}79fn main() -> minui::Result<()> {
80 // Create a new application instance with unit state `()`.
81 // This is the simplest case - we don't need to track any application state.
82 // For more complex apps, you'd pass a struct like `MyAppState { ... }`.
83 let mut app = App::new(())?;
84
85 // Run the application with update and draw closures.
86 // The app handles the event loop, window management, and rendering for you.
87 app.run(
88 // ========== UPDATE CLOSURE ==========
89 // Called whenever there's an input event (keyboard, mouse, etc.)
90 // Purpose: Update application state based on user input
91 // Returns: true to continue running, false to exit
92 |_state, event| {
93 // Handle the 'q' key to quit the application.
94 //
95 // The keyboard handler may emit:
96 // - `Event::KeyWithModifiers(KeyKind::Char('q'))` (modifier-aware), or
97 // - `Event::Character('q')` (legacy fallback).
98 match event {
99 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
100 Event::Character('q') => false,
101 _ => true,
102 }
103 },
104 // ========== DRAW CLOSURE ==========
105 // Called after each update to render the current application state
106 // Purpose: Draw the UI based on the current state
107 // The window parameter is where you draw everything
108 |_state, window| {
109 // Create a simple label widget centered on the screen
110 let label = Label::new("Press 'q' to quit").with_alignment(Alignment::Center);
111
112 // Draw the label to the window
113 // MinUI handles positioning, sizing, and rendering automatically
114 label.draw(window)?;
115
116 // Flush buffered rendering (App no longer auto-flushes after draw)
117 window.flush()?;
118
119 // Return Ok(()) to indicate drawing succeeded
120 Ok(())
121 },
122 )?;
123
124 // If we reach here, the app exited successfully
125 Ok(())
126}11fn main() -> minui::Result<()> {
12 let mut app = App::new(())?;
13
14 app.run(
15 |_state, event| {
16 // Return false to exit.
17 //
18 // The keyboard handler may emit:
19 // - `Event::KeyWithModifiers(KeyKind::Char('q'))` (modifier-aware), or
20 // - `Event::Character('q')` (legacy fallback).
21 match event {
22 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
23 Event::Character('q') => false,
24 _ => true,
25 }
26 },
27 |_state, window| {
28 // Display title at the top
29 window.write_str(0, 0, "Color Demo (press 'q' to quit)")?;
30
31 // Demonstrate all available foreground colors on black background
32 let colors = [
33 Color::Red,
34 Color::Green,
35 Color::Yellow,
36 Color::Blue,
37 Color::Magenta,
38 Color::Cyan,
39 Color::White,
40 ];
41
42 // Display each color name in its corresponding color
43 for (i, &color) in colors.iter().enumerate() {
44 let color_pair = ColorPair::new(color, Color::Black);
45 window.write_str_colored(
46 2, // Row 2
47 (i as u16) * 10, // Column 0, 10, 20, etc.
48 &format!("{:?}", color),
49 color_pair,
50 )?;
51 }
52
53 // Show practical examples of color usage for different types of messages
54 window.write_str_colored(
55 4,
56 0,
57 "Error: Something went wrong!",
58 ColorPair::new(Color::Red, Color::Black),
59 )?;
60
61 window.write_str_colored(
62 5,
63 0,
64 "Success: Operation completed!",
65 ColorPair::new(Color::Green, Color::Black),
66 )?;
67
68 window.write_str_colored(
69 6,
70 0,
71 "Warning: Disk space low",
72 ColorPair::new(Color::Yellow, Color::Black),
73 )?;
74
75 // Example of using background color for emphasis
76 window.write_str_colored(
77 7,
78 0,
79 "URGENT: System failure! jk :)",
80 ColorPair::new(Color::Black, Color::Red),
81 )?;
82
83 window.flush()?;
84 Ok(())
85 },
86 )?;
87
88 Ok(())
89}Sourcepub fn window(&self) -> &TerminalWindow
pub fn window(&self) -> &TerminalWindow
Returns an immutable reference to the underlying terminal window.
This is useful for advanced configuration (e.g. querying capabilities).
Sourcepub fn window_mut(&mut self) -> &mut TerminalWindow
pub fn window_mut(&mut self) -> &mut TerminalWindow
Returns a mutable reference to the underlying terminal window.
This enables advanced configuration prior to calling run(), for example:
- toggling mouse movement tracking
- adjusting keybind handlers
- overriding terminal capabilities (color fallback behavior)
Examples found in repository?
48fn main() -> minui::Result<()> {
49 let mut plain = TextInputState::new();
50 plain.set_focused(true);
51
52 let mut wrapped = TextInputState::new();
53 wrapped.set_focused(false);
54
55 let initial = State {
56 focus: Focus::Plain,
57 plain,
58 wrapped,
59 last_submit: String::from("(press Enter to submit focused field)"),
60
61 ui: UiScene::new(),
62
63 mouse_down: false,
64 dragging: false,
65 };
66
67 let mut app = App::new(initial)?;
68
69 // Ignore mouse-move spam; click/drag events still work.
70 app.window_mut().mouse_mut().set_movement_tracking(false);
71
72 app.run(
73 // ============================
74 // Update
75 // ============================
76 |state, event| {
77 // Apply common scene policies:
78 // - observe mouse position
79 // - tab traversal (if enabled; this demo uses manual toggle on Tab)
80 // - click-to-focus + mouse capture
81 let _effects = state.ui.apply_policies(&event);
82
83 // Ignore noisy events we don't use in this demo.
84 if matches!(event, Event::MouseMove { .. } | Event::Unknown) {
85 return true;
86 }
87
88 // Prefer modifier-aware key events first (the keyboard handler may emit these for most keys),
89 // with legacy fallback for older event paths.
90 if let Event::KeyWithModifiers(k) = event {
91 match k.key {
92 KeyKind::Char('q') => return false,
93 KeyKind::Tab => {
94 // Many backends emit modifier-aware Tab (`KeyWithModifiers`) but may not emit
95 // the legacy `Event::Tab`. Since this demo has exactly two fields, we keep a
96 // deterministic, backend-agnostic behavior: toggle focus between them.
97 //
98 // Note: UiScene's built-in tab traversal depends on per-frame registration order
99 // (available after draw), so we intentionally do not use it here.
100 toggle_focus(state);
101 return true;
102 }
103 KeyKind::Enter => {
104 state.last_submit = match state.focus {
105 Focus::Plain => format!("plain: {}", state.plain.text()),
106 Focus::Wrapped => format!("wrapped: {}", state.wrapped.text()),
107 };
108 return true;
109 }
110 _ => {}
111 }
112 }
113
114 // Quit (legacy fallback)
115 if matches!(event, Event::Character('q')) {
116 return false;
117 }
118
119 // Focus switching (legacy fallback)
120 if matches!(event, Event::Tab) {
121 // `UiScene` tab traversal relies on the current frame's registered focusables
122 // (which only exists after draw). In this demo, we keep a simple, deterministic
123 // fallback: toggle between the two fields.
124 toggle_focus(state);
125 return true;
126 }
127
128 // Mouse focus + selection, routed via InteractionCache.
129 match event {
130 Event::MouseClick { x, y, button: _ } => {
131 state.mouse_down = true;
132 state.dragging = false;
133
134 // Hit-test against the most recently drawn frame's registry.
135 let hit = state.ui.hit_test_id(x, y);
136
137 match hit {
138 Some(ID_PLAIN) => {
139 set_focus(state, Focus::Plain);
140 state.plain.click_set_cursor(x);
141 return true;
142 }
143 Some(ID_WRAPPED) => {
144 set_focus(state, Focus::Wrapped);
145 state.wrapped.click_set_cursor(x);
146 return true;
147 }
148 _ => {
149 // Click outside: cancel drag/selection tracking.
150 state.mouse_down = false;
151 state.dragging = false;
152 return true;
153 }
154 }
155 }
156 Event::MouseDrag { x, y: _, button: _ } => {
157 if !state.mouse_down {
158 return true;
159 }
160 state.dragging = true;
161
162 // Selection clamps to the field bounds internally.
163 match state.focus {
164 Focus::Plain => {
165 state.plain.drag_select_to(x);
166 return true;
167 }
168 Focus::Wrapped => {
169 state.wrapped.drag_select_to(x);
170 return true;
171 }
172 }
173 }
174 Event::MouseRelease { x, y: _, button: _ } => {
175 // Finalize drag (one last update on release), then stop tracking.
176 if state.mouse_down && state.dragging {
177 match state.focus {
178 Focus::Plain => state.plain.drag_select_to(x),
179 Focus::Wrapped => state.wrapped.drag_select_to(x),
180 }
181 }
182
183 state.mouse_down = false;
184 state.dragging = false;
185 return true;
186 }
187 _ => {}
188 }
189
190 // Submit on Enter: copy current focused value to output area (legacy fallback)
191 if matches!(event, Event::Enter) {
192 state.last_submit = match state.focus {
193 Focus::Plain => format!("plain: {}", state.plain.text()),
194 Focus::Wrapped => format!("wrapped: {}", state.wrapped.text()),
195 };
196 return true;
197 }
198
199 // Route remaining events to the focused field.
200 let consumed = match state.focus {
201 Focus::Plain => state.plain.handle_event(event),
202 Focus::Wrapped => state.wrapped.handle_event(event),
203 };
204
205 // If the input didn't consume it, ignore
206 consumed
207 },
208 // ============================
209 // Draw
210 // ============================
211 |state, window| {
212 let (w, h) = window.get_size();
213
214 // Begin a fresh immediate-mode scene frame (clears registrations, focusables, owners).
215 state.ui.begin_frame();
216
217 // Clear any stale cursor request; focused inputs will request one during draw.
218 window.clear_cursor_request();
219
220 // Layout constants
221 let margin: u16 = 2;
222 let row_gap: u16 = 2;
223
224 // Header
225 window.write_str(
226 0,
227 0,
228 "TextInput Demo — click fields, type, Enter to submit, Tab to switch, q to quit",
229 )?;
230
231 // Compute field geometry
232 let field_w = w.saturating_sub(margin * 2);
233
234 // Plain field at y = 2.
235 let plain_y = 2;
236 let plain = TextInput::new()
237 .with_position(margin, plain_y)
238 .with_width(field_w)
239 .with_border(true)
240 .with_placeholder("Plain input (click to focus)…");
241
242 // Wrapped-in-container field below.
243 let wrapped_container_y = plain_y + row_gap + 2;
244
245 // Fixed-height container; input sits on the content line.
246 let container_h: u16 = 3;
247
248 let container = Container::new()
249 .with_position_and_size(margin, wrapped_container_y, field_w, container_h)
250 .with_border()
251 .with_border_chars(BorderChars::single_line())
252 .with_border_color(ColorPair::new(Color::LightCyan, Color::Transparent))
253 .with_title("Wrapped in Container")
254 .with_title_alignment(TitleAlignment::Left)
255 .with_padding(minui::widgets::ContainerPadding::uniform(0));
256
257 // Output area near bottom
258 let output_y = h.saturating_sub(3);
259 let output_w = w.saturating_sub(margin * 2);
260 let output_prefix = "Last submit: ";
261 let output_text = format!("{}{}", output_prefix, state.last_submit);
262 let output_line = fit_to_cells(&output_text, output_w, TabPolicy::SingleCell, true);
263
264 // Draw + register plain field
265 // NOTE: `TextInput::draw_with_id(...)` registers into an InteractionCache, so we pass
266 // the underlying cache from UiScene.
267 plain.draw_with_id(window, &mut state.plain, state.ui.cache_mut(), ID_PLAIN)?;
268
269 // Draw container + wrapped field
270 container.draw(window)?;
271
272 let inner_x = margin + 1;
273 let inner_y = wrapped_container_y + 1;
274 let inner_w = field_w.saturating_sub(2);
275
276 let wrapped = TextInput::new()
277 .with_position(inner_x, inner_y)
278 .with_width(inner_w)
279 .with_border(false)
280 .with_placeholder("Wrapped input (click to focus)…");
281
282 // Draw + register wrapped field
283 wrapped.draw_with_id(window, &mut state.wrapped, state.ui.cache_mut(), ID_WRAPPED)?;
284
285 // Output area box
286 window.write_str_colored(
287 output_y.saturating_sub(1),
288 margin,
289 "─ Output ─",
290 ColorPair::new(Color::Yellow, Color::Transparent),
291 )?;
292 window.write_str_colored(
293 output_y,
294 margin,
295 &output_line,
296 ColorPair::new(Color::LightGray, Color::Transparent),
297 )?;
298 window.write_str_colored(
299 output_y + 1,
300 margin,
301 "Tip: Tab switches focus • Enter copies focused text here",
302 ColorPair::new(Color::DarkGray, Color::Transparent),
303 )?;
304
305 window.end_frame()?;
306 Ok(())
307 },
308 )?;
309
310 Ok(())
311}Sourcepub fn with_frame_rate(self, frame_rate: Duration) -> Self
pub fn with_frame_rate(self, frame_rate: Duration) -> Self
Enables a fixed frame rate (useful for animations and realtime-style terminal apps).
Your update function will receive Event::Frame at regular intervals.
Examples found in repository?
19fn main() -> minui::Result<()> {
20 let profile_store = Arc::new(Mutex::new(None));
21
22 let mut app = App::new(DemoState {
23 frame_count: 0,
24 latest_profile: Arc::clone(&profile_store),
25 })?
26 .with_frame_rate(Duration::from_millis(16))
27 .with_frame_budget(Duration::from_millis(16));
28
29 {
30 let store = Arc::clone(&profile_store);
31 app.set_frame_profile_hook(move |profile| {
32 if let Ok(mut slot) = store.lock() {
33 *slot = Some(*profile);
34 }
35 });
36 }
37
38 app.run(
39 |state, event| {
40 match event {
41 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => return false,
42 Event::Character('q') => return false,
43 Event::Frame => {
44 state.frame_count = state.frame_count.saturating_add(1);
45 }
46 _ => {}
47 }
48 true
49 },
50 |state, window| {
51 draw_header(window)?;
52 draw_code(window)?;
53 draw_profile_footer(state, window)?;
54 window.flush()?;
55 Ok(())
56 },
57 )
58}More examples
22fn main() -> minui::Result<()> {
23 // Start with player at position (5, 5)
24 let initial_state = MyCoolApp { x: 5, y: 5 };
25
26 // Enable a fixed frame rate (timed updates) every 100ms
27 let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(100));
28
29 // Run the main loop with update and draw functions
30 app.run(
31 // Update function: handle events and modify state
32 // Return false to exit, true to continue
33 |state, event| {
34 match event {
35 // Prefer modifier-aware key events (the keyboard handler may emit these for most keys).
36 Event::KeyWithModifiers(k) => match k.key {
37 KeyKind::Char('q') => return false,
38 KeyKind::Up => state.y = state.y.saturating_sub(1),
39 KeyKind::Down => state.y += 1,
40 KeyKind::Left => state.x = state.x.saturating_sub(1),
41 KeyKind::Right => state.x += 1,
42 _ => {}
43 },
44
45 // Legacy fallback.
46 Event::Character('q') => return false,
47
48 Event::KeyUp => state.y = state.y.saturating_sub(1),
49 Event::KeyDown => state.y += 1,
50 Event::KeyLeft => state.x = state.x.saturating_sub(1),
51 Event::KeyRight => state.x += 1,
52
53 Event::Frame => {
54 // Automatic movement every frame event (100ms)
55 state.x += 1;
56 }
57 _ => {}
58 }
59 // Keep running
60 true
61 },
62 // Draw function: render the current state
63 |state, window| {
64 window.write_str(state.y, state.x, "@")?;
65 window.write_str(0, 0, "Press 'q' to quit")?;
66 window.flush()?;
67 Ok(())
68 },
69 )?;
70
71 Ok(())
72}16fn main() -> minui::Result<()> {
17 let initial_state = MovementState { x: 2, y: 2 };
18
19 let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(50));
20
21 app.run(
22 |state, event| {
23 let (width, height) = (80u16, 24u16); // Reasonable defaults
24
25 match event {
26 // Prefer modifier-aware key events first (the keyboard handler may emit these for most keys).
27 Event::KeyWithModifiers(k) => match k.key {
28 KeyKind::Char('q') | KeyKind::Escape => return false,
29
30 KeyKind::Up => {
31 if state.y > 0 {
32 state.y -= 1;
33 }
34 }
35 KeyKind::Down => {
36 if state.y < height - 1 {
37 state.y += 1;
38 }
39 }
40 KeyKind::Left => {
41 if state.x > 0 {
42 state.x -= 1;
43 }
44 }
45 KeyKind::Right => {
46 if state.x < width - 1 {
47 state.x += 1;
48 }
49 }
50 _ => {}
51 },
52
53 // Legacy fallback.
54 Event::Character('q') | Event::Escape => return false,
55 Event::KeyUp => {
56 if state.y > 0 {
57 state.y -= 1;
58 }
59 }
60 Event::KeyDown => {
61 if state.y < height - 1 {
62 state.y += 1;
63 }
64 }
65 Event::KeyLeft => {
66 if state.x > 0 {
67 state.x -= 1;
68 }
69 }
70 Event::KeyRight => {
71 if state.x < width - 1 {
72 state.x += 1;
73 }
74 }
75
76 _ => {}
77 }
78
79 true
80 },
81 |state, window| {
82 let (width, height) = window.get_size();
83
84 // Draw instructions
85 let instructions =
86 Label::new("Use arrow keys to move, 'q' to quit").with_text_color(Color::Cyan);
87 instructions.draw(window)?;
88
89 // Draw the player character
90 window.write_str_colored(
91 state.y,
92 state.x,
93 "@",
94 ColorPair::new(Color::Green, Color::Transparent),
95 )?;
96
97 // Draw boundary indicators
98 window.write_str(
99 height - 1,
100 0,
101 &format!(
102 "Position: ({}, {}) | Terminal: {}x{}",
103 state.x, state.y, width, height
104 ),
105 )?;
106
107 window.flush()?;
108 Ok(())
109 },
110 )?;
111
112 Ok(())
113}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}Sourcepub fn with_frame_budget(self, budget: Duration) -> Self
pub fn with_frame_budget(self, budget: Duration) -> Self
Sets a soft frame-time budget used for profiling.
Frame pacing is controlled by App::with_frame_rate.
Examples found in repository?
19fn main() -> minui::Result<()> {
20 let profile_store = Arc::new(Mutex::new(None));
21
22 let mut app = App::new(DemoState {
23 frame_count: 0,
24 latest_profile: Arc::clone(&profile_store),
25 })?
26 .with_frame_rate(Duration::from_millis(16))
27 .with_frame_budget(Duration::from_millis(16));
28
29 {
30 let store = Arc::clone(&profile_store);
31 app.set_frame_profile_hook(move |profile| {
32 if let Ok(mut slot) = store.lock() {
33 *slot = Some(*profile);
34 }
35 });
36 }
37
38 app.run(
39 |state, event| {
40 match event {
41 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => return false,
42 Event::Character('q') => return false,
43 Event::Frame => {
44 state.frame_count = state.frame_count.saturating_add(1);
45 }
46 _ => {}
47 }
48 true
49 },
50 |state, window| {
51 draw_header(window)?;
52 draw_code(window)?;
53 draw_profile_footer(state, window)?;
54 window.flush()?;
55 Ok(())
56 },
57 )
58}Sourcepub fn set_frame_profile_hook<F>(&mut self, hook: F)where
F: FnMut(&FrameProfile) + 'static,
pub fn set_frame_profile_hook<F>(&mut self, hook: F)where
F: FnMut(&FrameProfile) + 'static,
Installs a per-frame profiling hook.
Examples found in repository?
19fn main() -> minui::Result<()> {
20 let profile_store = Arc::new(Mutex::new(None));
21
22 let mut app = App::new(DemoState {
23 frame_count: 0,
24 latest_profile: Arc::clone(&profile_store),
25 })?
26 .with_frame_rate(Duration::from_millis(16))
27 .with_frame_budget(Duration::from_millis(16));
28
29 {
30 let store = Arc::clone(&profile_store);
31 app.set_frame_profile_hook(move |profile| {
32 if let Ok(mut slot) = store.lock() {
33 *slot = Some(*profile);
34 }
35 });
36 }
37
38 app.run(
39 |state, event| {
40 match event {
41 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => return false,
42 Event::Character('q') => return false,
43 Event::Frame => {
44 state.frame_count = state.frame_count.saturating_add(1);
45 }
46 _ => {}
47 }
48 true
49 },
50 |state, window| {
51 draw_header(window)?;
52 draw_code(window)?;
53 draw_profile_footer(state, window)?;
54 window.flush()?;
55 Ok(())
56 },
57 )
58}Sourcepub fn clear_frame_profile_hook(&mut self)
pub fn clear_frame_profile_hook(&mut self)
Removes the current profiling hook.
Sourcepub fn run<U, D>(&mut self, update: U, draw: D) -> Result<()>
pub fn run<U, D>(&mut self, update: U, draw: D) -> Result<()>
Runs the main application loop.
update: Called for each event. Returnfalseto exit.draw: Called to render the current state.
Examples found in repository?
18fn main() -> minui::Result<()> {
19 let mut app = App::new(())?;
20
21 app.run(
22 |_state, event| {
23 // Quit on 'q' (supports both modifier-aware and legacy events).
24 match event {
25 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
26 Event::Character('q') => false,
27 _ => true,
28 }
29 },
30 |_state, window| {
31 let (w, h) = window.get_size();
32 create_app_layout(w, h).draw(window)?;
33 window.flush()?;
34 Ok(())
35 },
36 )?;
37
38 Ok(())
39}More examples
36fn main() -> minui::Result<()> {
37 let mut app = App::new(())?;
38
39 app.run(
40 |_state, event| {
41 // Return false to exit.
42 //
43 // The keyboard handler may emit:
44 // - `Event::KeyWithModifiers(KeyKind::Char('q'))` (modifier-aware), or
45 // - `Event::Character('q')` (legacy fallback).
46 match event {
47 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
48 Event::Character('q') => false,
49 _ => true,
50 }
51 },
52 |_state, window| {
53 let (width, height) = window.get_size();
54
55 // Title
56 window.write_str(0, 0, "RGB Color & Gradient Demo (press 'q' to quit)")?;
57
58 // --- All drawing calls happen here ---
59 draw_rainbow_gradient(window, 2, width)?;
60 draw_color_themes(window, 6)?;
61 draw_vertical_gradient(window, 12, width, height)?;
62 draw_color_blocks(window, height)?;
63
64 window.end_frame()?;
65 Ok(())
66 },
67 )?;
68
69 Ok(())
70}19fn main() -> minui::Result<()> {
20 let profile_store = Arc::new(Mutex::new(None));
21
22 let mut app = App::new(DemoState {
23 frame_count: 0,
24 latest_profile: Arc::clone(&profile_store),
25 })?
26 .with_frame_rate(Duration::from_millis(16))
27 .with_frame_budget(Duration::from_millis(16));
28
29 {
30 let store = Arc::clone(&profile_store);
31 app.set_frame_profile_hook(move |profile| {
32 if let Ok(mut slot) = store.lock() {
33 *slot = Some(*profile);
34 }
35 });
36 }
37
38 app.run(
39 |state, event| {
40 match event {
41 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => return false,
42 Event::Character('q') => return false,
43 Event::Frame => {
44 state.frame_count = state.frame_count.saturating_add(1);
45 }
46 _ => {}
47 }
48 true
49 },
50 |state, window| {
51 draw_header(window)?;
52 draw_code(window)?;
53 draw_profile_footer(state, window)?;
54 window.flush()?;
55 Ok(())
56 },
57 )
58}22fn main() -> minui::Result<()> {
23 // Start with player at position (5, 5)
24 let initial_state = MyCoolApp { x: 5, y: 5 };
25
26 // Enable a fixed frame rate (timed updates) every 100ms
27 let mut app = App::new(initial_state)?.with_frame_rate(Duration::from_millis(100));
28
29 // Run the main loop with update and draw functions
30 app.run(
31 // Update function: handle events and modify state
32 // Return false to exit, true to continue
33 |state, event| {
34 match event {
35 // Prefer modifier-aware key events (the keyboard handler may emit these for most keys).
36 Event::KeyWithModifiers(k) => match k.key {
37 KeyKind::Char('q') => return false,
38 KeyKind::Up => state.y = state.y.saturating_sub(1),
39 KeyKind::Down => state.y += 1,
40 KeyKind::Left => state.x = state.x.saturating_sub(1),
41 KeyKind::Right => state.x += 1,
42 _ => {}
43 },
44
45 // Legacy fallback.
46 Event::Character('q') => return false,
47
48 Event::KeyUp => state.y = state.y.saturating_sub(1),
49 Event::KeyDown => state.y += 1,
50 Event::KeyLeft => state.x = state.x.saturating_sub(1),
51 Event::KeyRight => state.x += 1,
52
53 Event::Frame => {
54 // Automatic movement every frame event (100ms)
55 state.x += 1;
56 }
57 _ => {}
58 }
59 // Keep running
60 true
61 },
62 // Draw function: render the current state
63 |state, window| {
64 window.write_str(state.y, state.x, "@")?;
65 window.write_str(0, 0, "Press 'q' to quit")?;
66 window.flush()?;
67 Ok(())
68 },
69 )?;
70
71 Ok(())
72}79fn main() -> minui::Result<()> {
80 // Create a new application instance with unit state `()`.
81 // This is the simplest case - we don't need to track any application state.
82 // For more complex apps, you'd pass a struct like `MyAppState { ... }`.
83 let mut app = App::new(())?;
84
85 // Run the application with update and draw closures.
86 // The app handles the event loop, window management, and rendering for you.
87 app.run(
88 // ========== UPDATE CLOSURE ==========
89 // Called whenever there's an input event (keyboard, mouse, etc.)
90 // Purpose: Update application state based on user input
91 // Returns: true to continue running, false to exit
92 |_state, event| {
93 // Handle the 'q' key to quit the application.
94 //
95 // The keyboard handler may emit:
96 // - `Event::KeyWithModifiers(KeyKind::Char('q'))` (modifier-aware), or
97 // - `Event::Character('q')` (legacy fallback).
98 match event {
99 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
100 Event::Character('q') => false,
101 _ => true,
102 }
103 },
104 // ========== DRAW CLOSURE ==========
105 // Called after each update to render the current application state
106 // Purpose: Draw the UI based on the current state
107 // The window parameter is where you draw everything
108 |_state, window| {
109 // Create a simple label widget centered on the screen
110 let label = Label::new("Press 'q' to quit").with_alignment(Alignment::Center);
111
112 // Draw the label to the window
113 // MinUI handles positioning, sizing, and rendering automatically
114 label.draw(window)?;
115
116 // Flush buffered rendering (App no longer auto-flushes after draw)
117 window.flush()?;
118
119 // Return Ok(()) to indicate drawing succeeded
120 Ok(())
121 },
122 )?;
123
124 // If we reach here, the app exited successfully
125 Ok(())
126}11fn main() -> minui::Result<()> {
12 let mut app = App::new(())?;
13
14 app.run(
15 |_state, event| {
16 // Return false to exit.
17 //
18 // The keyboard handler may emit:
19 // - `Event::KeyWithModifiers(KeyKind::Char('q'))` (modifier-aware), or
20 // - `Event::Character('q')` (legacy fallback).
21 match event {
22 Event::KeyWithModifiers(k) if matches!(k.key, KeyKind::Char('q')) => false,
23 Event::Character('q') => false,
24 _ => true,
25 }
26 },
27 |_state, window| {
28 // Display title at the top
29 window.write_str(0, 0, "Color Demo (press 'q' to quit)")?;
30
31 // Demonstrate all available foreground colors on black background
32 let colors = [
33 Color::Red,
34 Color::Green,
35 Color::Yellow,
36 Color::Blue,
37 Color::Magenta,
38 Color::Cyan,
39 Color::White,
40 ];
41
42 // Display each color name in its corresponding color
43 for (i, &color) in colors.iter().enumerate() {
44 let color_pair = ColorPair::new(color, Color::Black);
45 window.write_str_colored(
46 2, // Row 2
47 (i as u16) * 10, // Column 0, 10, 20, etc.
48 &format!("{:?}", color),
49 color_pair,
50 )?;
51 }
52
53 // Show practical examples of color usage for different types of messages
54 window.write_str_colored(
55 4,
56 0,
57 "Error: Something went wrong!",
58 ColorPair::new(Color::Red, Color::Black),
59 )?;
60
61 window.write_str_colored(
62 5,
63 0,
64 "Success: Operation completed!",
65 ColorPair::new(Color::Green, Color::Black),
66 )?;
67
68 window.write_str_colored(
69 6,
70 0,
71 "Warning: Disk space low",
72 ColorPair::new(Color::Yellow, Color::Black),
73 )?;
74
75 // Example of using background color for emphasis
76 window.write_str_colored(
77 7,
78 0,
79 "URGENT: System failure! jk :)",
80 ColorPair::new(Color::Black, Color::Red),
81 )?;
82
83 window.flush()?;
84 Ok(())
85 },
86 )?;
87
88 Ok(())
89}