I have a tui app where a user is presented with some choices through a list. Once they navigate to the choice they want and hit enter I'd like to take them to the "next" screen.
It's more complicated than just clearning existing text and printing new one because I also need to replace keybindings and basically start a new tui-rs loop. More below.
Code for Screen 1:
pub fn draw_screen() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let events = Events::new();
loop {
terminal.draw(|f| {
// user shown a list they can navigate through using arrow keys
});
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Char('\n') => {
// this is where I need to "send" them to a new screen
}
Key::Down => {
// my_list won't exist on next screen
my_list.items.next();
}
Key::Up => {
my_list.items.previous();
}
_ => {}
},
_ => {}
}
}
Ok(())
}
As can be seen the keybindings at the bottom are specific to this screen. Eg on the next screen there's not going to be a my_list and instead there might be a my_another_list or my_box or nothing at all.
So if all I did was clear the text, I'd still be left inside the same loop with the same keybindings - doesn't work.
What's the right way to initiate a new loop with fresh keybindings?
Related
I want contents' size in a imgui window looks always same, but on resizing OS window, imgui window size and its contents scale too. Actually contents looks 'wrong' when resized, this is scaled down figure of 'hello world' example from imgui-rs repository.
I think I should achieve it by using glViewport, but it looks like I have no access to the function or equivalent things, and ContextWrapper::resize has no effect.
Event::NewEvents(_) => {
let now = Instant::now();
imgui.io_mut().update_delta_time(now - last_frame);
last_frame = now;
}
Event::MainEventsCleared => {
let gl_window = display.gl_window();
platform
.prepare_frame(imgui.io_mut(), gl_window.window())
.expect("Failed to prepare frame");
gl_window.window().request_redraw();
}
Event::RedrawRequested(_) => {
let ui = imgui.new_frame();
let mut run = true;
run_ui(&mut run, ui);
if !run {
*control_flow = ControlFlow::Exit;
}
let gl_window = display.gl_window();
let mut target = display.draw();
target.clear_color_srgb(1.0, 1.0, 1.0, 1.0);
platform.prepare_render(ui, gl_window.window());
let draw_data = imgui.render();
renderer
.render(&mut target, draw_data)
.expect("Rendering failed");
target.finish().expect("Failed to swap buffers");
}
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
display.gl_window().resize(size);
}
What should I do to keep contents' size constant and to render things correctly?
I'm using imgui with glium and winit.
I am a blind Rust learner and i'm trying to make a simple audiogame engine for blind people but in rust.
Now I'm trying to make a simple window. A game for blind uses only audio and keyboard, so i have a question.
I'm trying to create an empty window and catch key presses to be able to make a self voiced user interface and game interaction using keyboard only, without mouce. what's the best solution for that?
I tryed to use winit and winit input helper, but maybe there is a better way to do that?
use winit::event::VirtualKeyCode;
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit_input_helper::WinitInputHelper;
pub fn show_window() {
let mut input = WinitInputHelper::new();
let event_loop = EventLoop::new();
let _window = WindowBuilder::new().build(&event_loop).unwrap();
event_loop.run(move |event, _, control_flow| {
// Pass every event to the WindowInputHelper.
// It will return true when the last event has been processed and it is time to run your application logic.
if input.update(&event) {
// query keypresses this update
if input.key_pressed_os(VirtualKeyCode::A) {
println!("The 'A' key was pressed on the keyboard (OS repeating)");
}
if input.key_pressed(VirtualKeyCode::A) {
println!("The 'A' key was pressed on the keyboard");
}
if input.key_released(VirtualKeyCode::Q) || input.quit() {
*control_flow = ControlFlow::Exit;
return;
}
}
});
}
I'm attempting to do something that I feel is pretty basic: I have a pulldown, and I'd like the onchange event for that pulldown to cause the program to fetch some data from the backend based on the user's input. (And then, you know, give the user more options based on the first thing they picked. Really simple, and seems like I ought to have been able to find an easy way to do this.)
Full code for this minimal (failing) example is at: https://github.com/djmcmath/broken-yew
But the relevant bit, which doesn't behave correctly, is below:
The view function renders, delightfully, an iterated list. I pass in a callback, so it knows what to do on the "onchange" event.
The callback gets executed, which makes me very happy. But it isn't calling the Msg::GetData. This compiles, which is nice, but it doesn't work, which is less nice.
I've spent, I'm ashamed to admit, several weeks of my spare time fighting with this. I think it has something to do with scopes and lifetimes. I think that the way I'm making this compile -- by cloning the context and using "move" disconnects it from the actual context that I need to make this work. But every variation on the theme that I've been able to find in examples and references complains about scope or lifetimes.
Thanks in advance for the help.
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::GetData(value) => {
log::info!("Start 'fetch' with user-selected value: {}", value);
ctx.link().send_future(async {
match fetch_markdown("url_shortened_for_clarity").await {
Ok(md) => Msg::SetMarkdownFetchState(FetchState::Success(md)),
Err(err) => Msg::SetMarkdownFetchState(FetchState::Failed(err)),
}
});
false
},
Msg::SetMarkdownFetchState(fetch_state) => {
let mut wr = WebReturn { term_id: 0, dow: 0, dep_time_num: 0 };
match fetch_state {
FetchState::Success(s) => { wr = serde_json::from_str(&s).expect(&format!("Poorly formatted JSON! {}", s).to_string()); },
FetchState::Failed(f) => { log::info!("Fetch failed: {}", f); },
FetchState::NotFetching => {},
FetchState::Fetching => {}
};
log::info!("term_id (3) : {}, dep_time_num (12000) : {}, and dow (3) : {}", wr.term_id, wr.dep_time_num, wr.dow);
true
}
}
}
fn view(&self, ctx:&Context<Self>) -> Html {
let ctx_link = ctx.link().clone();
let my_callback: Callback<String> = Callback::from(move |value: String| {
let val_as_num = value.parse::<i32>().unwrap_or(0);
log::info!("Returned value: {}", val_as_num);
ctx_link.callback(|val_as_num: i32| Msg::GetData(val_as_num));
});
html! {
<div>
{ self.render_list(&self.props.term_list, my_callback) }
</div>
}
}
This line does not "call back" to your component, it creates a callback and then doesn't call it:
ctx_link.callback(|val_as_num: i32| Msg::GetData(val_as_num));
You need to instead call .send_message() in your callback or, better yet, create your original callback with .callback():
let my_callback = ctx_link.callback(|value: String| {
let val_as_num = value.parse::<i32>().unwrap_or(0);
log::info!("Returned value: {}", val_as_num);
Msg::GetData(val_as_num)
});
I'm using Yew to program a theme switcher that by clicking cycles through different themes.
This is my update function. It gets the current theme which is stored in shared state, depending on what would come next inside theme_cycle the theme value inside the shared state gets set to it.
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::ChangeTheme => {
let theme_cycle: [&str; 3] = ["light", "dark", "rust"];
let current_theme = self.props.handle.state().theme.clone();
// eval next theme
let next_theme = match theme_cycle.iter().position(|x| x == ¤t_theme) {
None => theme_cycle[0].to_string(),
Some(i) => {
if i >= (current_theme.len() - 1) {
theme_cycle[0].to_string()
} else {
theme_cycle[i + 1].to_string()
}
},
};
// set next theme
self.props.handle.reduce(move |state| state.theme = next_theme.clone());
// store it inside localstorage
},
Msg::ToggleLangDropdown => self.show_dropdown = !self.show_dropdown,
};
true
}
But if the theme val inside shared state is "rust" and I click the button again that calls Msg::ChangeTheme, the theme should be set to "light" but instead my code panics and I get an "Uncaught Error: undefined" inside the Browser Console.
I've found a workaround; instead of using an array and accessing the values, I've tried to do the same task but just with iterators and made sure that the update function takes no ownership of any variable outside the function itself (I don't really know if that is really necessary though...)
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::ChangeTheme => {
let theme_cycle = ["light".to_string(), "dark".to_string(), "rust".to_string()];
let current_theme = self.props.handle.state().theme.clone();
let indexof_current_theme = match theme_cycle.iter().position(|x| x.to_string() == current_theme) {
None => 0,
Some(x) => x.clone(),
};
let next_theme = match theme_cycle.iter().nth(indexof_current_theme + 1) {
None => theme_cycle.iter().nth(0).unwrap().clone(),
Some(x) => x.clone(),
};
self.props.handle.reduce(move |state| state.theme = next_theme.to_string());
},
Msg::ToggleLangDropdown => self.show_lang_dropdown = !self.show_lang_dropdown,
Msg::ToggleThemeDropdown => self.show_theme_dropdown = !self.show_theme_dropdown,
};
true
}
Still would be cool if anyone knows what I did wrong in my first attempt.
I would like to detect a keydown event in Rust and then check if a combination of keys is pressed, in order to do further actions based on that.
So basically support keyboard shortcuts in my Rust application.
I've looked at some crates for example ncurses but they did not match my requirements...
Best solution for ANSI terminals (Linux, macOS)
If you don't need support for Windows then the best is termion.
It's a library for manipulating the terminal. In which you can detect key events and even keyboard shortcuts. And it's also really lightweight! Only 22.78 kB (as of version 1.5.5).
Here's a quick program I put together to showcase few shortcuts.
Add this code to main.rs, add termion = "1.5.5" to Cargo.toml and start it with cargo run!
use std::io::{stdin, stdout, Write};
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode;
fn main() {
let stdin = stdin();
//setting up stdout and going into raw mode
let mut stdout = stdout().into_raw_mode().unwrap();
//printing welcoming message, clearing the screen and going to left top corner with the cursor
write!(stdout, r#"{}{}ctrl + q to exit, ctrl + h to print "Hello world!", alt + t to print "termion is cool""#, termion::cursor::Goto(1, 1), termion::clear::All)
.unwrap();
stdout.flush().unwrap();
//detecting keydown events
for c in stdin.keys() {
//clearing the screen and going to top left corner
write!(
stdout,
"{}{}",
termion::cursor::Goto(1, 1),
termion::clear::All
)
.unwrap();
//i reckon this speaks for itself
match c.unwrap() {
Key::Ctrl('h') => println!("Hello world!"),
Key::Ctrl('q') => break,
Key::Alt('t') => println!("termion is cool"),
_ => (),
}
stdout.flush().unwrap();
}
}
Cross Platform Solution
If you need to support Windows and all other platforms, then you can use crossterm. It's a pretty decent library and quite heavier than termion. It's 98.06 kB (as of version 0.16.0).
Here's the same program as above but written using crossterm.
Add this code to main.rs, add crossterm = "0.16.0" to Cargo.toml and try it with cargo run!
//importing in execute! macro
#[macro_use]
extern crate crossterm;
use crossterm::cursor;
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::style::Print;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
use std::io::{stdout, Write};
fn main() {
let mut stdout = stdout();
//going into raw mode
enable_raw_mode().unwrap();
//clearing the screen, going to top left corner and printing welcoming message
execute!(stdout, Clear(ClearType::All), cursor::MoveTo(0, 0), Print(r#"ctrl + q to exit, ctrl + h to print "Hello world", alt + t to print "crossterm is cool""#))
.unwrap();
//key detection
loop {
//going to top left corner
execute!(stdout, cursor::MoveTo(0, 0)).unwrap();
//matching the key
match read().unwrap() {
//i think this speaks for itself
Event::Key(KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
//clearing the screen and printing our message
}) => execute!(stdout, Clear(ClearType::All), Print("Hello world!")).unwrap(),
Event::Key(KeyEvent {
code: KeyCode::Char('t'),
modifiers: KeyModifiers::ALT,
}) => execute!(stdout, Clear(ClearType::All), Print("crossterm is cool")).unwrap(),
Event::Key(KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
}) => break,
_ => (),
}
}
//disabling raw mode
disable_raw_mode().unwrap();
}
I'm not going to lie, this is a bit harder to read than the termion solution, but it does the same job. I have no prior experience with crossterm so this code may actually not be the best but it's decent.
Looking for a way to detect only key press without any modifier (Shift, Control, Alt)? Check this simplified code:
//-- code --
loop {
//--code--
match read().unwrap() {
Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::NONE,
}) => //--code--
}
//--code--
}
//--code--
The important part here is the use of KeyModifiers::NONE.
You could use console as a simple cross-platform solution.
use console::Term;
fn main() {
let stdout = Term::buffered_stdout();
'game_loop: loop {
if let Ok(character) = stdout.read_char() {
match character {
'w' => todo!("Up"),
'a' => todo!("Left"),
's' => todo!("Down"),
'd' => todo!("Right"),
_ => break 'game_loop,
}
}
}
}
The snippet above shows a basic example for matching common movement characters for a platform.