Skip to main content

App

Struct App 

Source
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>

Source

pub fn new(initial_state: S) -> Result<Self>

Creates a new app with the given initial state.

Examples found in repository?
examples/widget_usage.rs (line 19)
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
Hide additional examples
examples/custom_colors.rs (line 37)
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}
examples/syntax_profile_demo.rs (lines 22-25)
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}
examples/app_runner.rs (line 27)
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}
examples/basic_usage.rs (line 83)
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}
examples/color_usage.rs (line 12)
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}
Source

pub fn window(&self) -> &TerminalWindow

Returns an immutable reference to the underlying terminal window.

This is useful for advanced configuration (e.g. querying capabilities).

Source

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?
examples/text_input_demo.rs (line 70)
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}
Source

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?
examples/syntax_profile_demo.rs (line 26)
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
Hide additional examples
examples/app_runner.rs (line 27)
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}
examples/movement.rs (line 19)
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}
examples/input_demo.rs (line 37)
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}
Source

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?
examples/syntax_profile_demo.rs (line 27)
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}
Source

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?
examples/syntax_profile_demo.rs (lines 31-35)
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}
Source

pub fn clear_frame_profile_hook(&mut self)

Removes the current profiling hook.

Source

pub fn run<U, D>(&mut self, update: U, draw: D) -> Result<()>
where U: FnMut(&mut S, Event) -> bool, D: FnMut(&mut S, &mut dyn Window) -> Result<()>,

Runs the main application loop.

  • update: Called for each event. Return false to exit.
  • draw: Called to render the current state.
Examples found in repository?
examples/widget_usage.rs (lines 21-36)
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
Hide additional examples
examples/custom_colors.rs (lines 39-67)
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}
examples/syntax_profile_demo.rs (lines 38-57)
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}
examples/app_runner.rs (lines 30-69)
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}
examples/basic_usage.rs (lines 87-122)
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}
examples/color_usage.rs (lines 14-86)
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}

Auto Trait Implementations§

§

impl<S> !RefUnwindSafe for App<S>

§

impl<S> !Send for App<S>

§

impl<S> !Sync for App<S>

§

impl<S> !UnwindSafe for App<S>

§

impl<S> Freeze for App<S>
where S: Freeze,

§

impl<S> Unpin for App<S>
where S: Unpin,

§

impl<S> UnsafeUnpin for App<S>
where S: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.