-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #29 from warpy-ai/28-featue-create-a-simple-menu-list
28 featue create a simple menu list
- Loading branch information
Showing
12 changed files
with
330 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[package] | ||
name = "rustubble" | ||
version = "0.1.2" | ||
version = "0.1.3" | ||
edition = "2021" | ||
authors = ["Lucas Oliveira <[email protected]>"] # List of crate authors. | ||
description = "A brief description of what your crate does." | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; | ||
use rustubble::menu_list::{handle_menu_list, Menu}; | ||
use std::io; | ||
|
||
fn main() -> Result<(), io::Error> { | ||
enable_raw_mode()?; | ||
|
||
let mut new_menu = Menu::new( | ||
"Main Menu".to_string(), | ||
"Select an option:".to_string(), | ||
vec![ | ||
"Option 1".to_string(), | ||
"Option 2".to_string(), | ||
"Option 3".to_string(), | ||
"Option 4".to_string(), | ||
], | ||
); | ||
|
||
let (x, y) = (5, 5); | ||
|
||
let selected_menu = handle_menu_list(&mut new_menu, x, y); | ||
|
||
println!("Selected Menu: {:?}", selected_menu); | ||
disable_raw_mode() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
use std::io; | ||
|
||
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers}; | ||
use ratatui::{ | ||
backend::{Backend, CrosstermBackend}, | ||
layout::{Constraint, Direction, Layout, Rect}, | ||
style::{Color, Modifier, Style, Stylize}, | ||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, | ||
Terminal, | ||
}; | ||
|
||
use crate::{command::CommandInfo, help::HelpComponent}; | ||
|
||
#[derive(Clone, Debug)] | ||
struct MenuItem { | ||
name: String, | ||
selected: bool, | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
pub struct Menu { | ||
title: String, | ||
subtitle: String, | ||
items: Vec<MenuItem>, | ||
selection_state: ListState, | ||
} | ||
|
||
impl Menu { | ||
pub fn new(title: String, subtitle: String, items: Vec<String>) -> Self { | ||
let mut state = ListState::default(); | ||
state.select(Some(0)); // Initialize the cursor at the first item | ||
|
||
let menu_items = items | ||
.into_iter() | ||
.map(|item| MenuItem { | ||
name: item, | ||
selected: false, | ||
}) | ||
.collect(); | ||
|
||
Self { | ||
title, | ||
subtitle, | ||
items: menu_items, | ||
selection_state: state, | ||
} | ||
} | ||
|
||
pub fn render<B: Backend>( | ||
&self, | ||
terminal: &mut Terminal<B>, | ||
area: Rect, | ||
help_component: &mut HelpComponent, | ||
) { | ||
terminal | ||
.draw(|f| { | ||
let chunks = Layout::default() | ||
.direction(Direction::Vertical) | ||
.constraints( | ||
[ | ||
Constraint::Length(1), | ||
Constraint::Length(2), | ||
Constraint::Max(10), | ||
Constraint::Length(3), | ||
] | ||
.as_ref(), | ||
) | ||
.split(area); | ||
|
||
let title_widget = format!("{}", self.title); | ||
let title = Paragraph::new(title_widget.as_str()) | ||
.style(Style::default().add_modifier(Modifier::BOLD)) | ||
.fg(Color::LightMagenta) | ||
.block(Block::default().borders(Borders::NONE)); | ||
f.render_widget(title, chunks[0]); | ||
|
||
let subtitle_widget = format!("{}", self.subtitle); | ||
let subtitle = Paragraph::new(subtitle_widget.as_str()) | ||
.style( | ||
Style::default() | ||
.add_modifier(Modifier::BOLD) | ||
.fg(Color::DarkGray), | ||
) | ||
.block(Block::default().borders(Borders::NONE)); | ||
f.render_widget(subtitle, chunks[1]); | ||
|
||
let items: Vec<ListItem> = self | ||
.items | ||
.iter() | ||
.map(|item| { | ||
let content = if item.selected { | ||
format!("✓ {}", item.name) | ||
} else { | ||
format!(" {}", item.name) | ||
}; | ||
ListItem::new(content) | ||
}) | ||
.collect(); | ||
|
||
//TODO: add color to symbol | ||
let symbol = "> "; | ||
let list = List::new(items) | ||
.block(Block::default().borders(Borders::NONE)) | ||
.highlight_style(Style::default().add_modifier(Modifier::BOLD)) | ||
.highlight_symbol(symbol) | ||
.scroll_padding(4); | ||
f.render_stateful_widget(list, chunks[2], &mut self.selection_state.clone()); | ||
//TODO: calculate the area and render widget help_component under list | ||
f.render_widget(help_component.clone(), chunks[3]); | ||
}) | ||
.unwrap(); | ||
} | ||
|
||
pub fn up(&mut self) { | ||
let i = match self.selection_state.selected() { | ||
Some(i) => { | ||
if i == 0 { | ||
self.items.len() - 1 | ||
} else { | ||
i - 1 | ||
} | ||
} | ||
None => 0, | ||
}; | ||
self.selection_state.select(Some(i)); | ||
} | ||
|
||
pub fn down(&mut self) { | ||
let i = match self.selection_state.selected() { | ||
Some(i) => { | ||
if i >= self.items.len() - 1 { | ||
0 | ||
} else { | ||
i + 1 | ||
} | ||
} | ||
None => 0, | ||
}; | ||
self.selection_state.select(Some(i)); | ||
} | ||
|
||
pub fn toggle_selection(&mut self) { | ||
if let Some(i) = self.selection_state.selected() { | ||
self.items[i].selected = !self.items[i].selected; | ||
} | ||
} | ||
|
||
// Add methods to handle key inputs: up, down, toggle selection, etc. | ||
} | ||
|
||
pub fn handle_menu_list(menu: &mut Menu, x: u16, y: u16) -> Option<String> { | ||
// Render the menu | ||
let stdout = io::stdout(); | ||
let backend = CrosstermBackend::new(stdout); | ||
let mut terminal = Terminal::new(backend).unwrap(); | ||
loop { | ||
terminal.clear().unwrap(); | ||
|
||
let commands = vec![ | ||
CommandInfo::new(KeyCode::Char('c'), KeyModifiers::CONTROL), | ||
CommandInfo::new(KeyCode::Char('q'), KeyModifiers::NONE), | ||
CommandInfo::new(KeyCode::Enter, KeyModifiers::NONE), | ||
CommandInfo::new(KeyCode::Down, KeyModifiers::NONE), | ||
CommandInfo::new(KeyCode::Up, KeyModifiers::NONE), | ||
]; | ||
|
||
let mut help_component = HelpComponent::new(commands, vec![]); | ||
|
||
menu.render(&mut terminal, Rect::new(x, y, 40, 50), &mut help_component); | ||
|
||
match read().unwrap() { | ||
Event::Key(KeyEvent { | ||
code: KeyCode::Char(c), | ||
modifiers, | ||
.. | ||
}) => { | ||
if c == 'j' { | ||
menu.down(); | ||
} | ||
if c == 'k' { | ||
menu.up(); | ||
} | ||
if c == 'q' { | ||
return None; | ||
} | ||
if modifiers.contains(KeyModifiers::CONTROL) && c == 't' { | ||
menu.toggle_selection(); | ||
} | ||
if modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { | ||
return None; | ||
} | ||
} | ||
Event::Key(KeyEvent { | ||
code: KeyCode::Up, .. | ||
}) => menu.up(), | ||
Event::Key(KeyEvent { | ||
code: KeyCode::Down, | ||
.. | ||
}) => menu.down(), | ||
Event::Key(KeyEvent { | ||
code: KeyCode::Enter, | ||
.. | ||
}) => { | ||
if let Some(i) = menu.selection_state.selected() { | ||
return Some(menu.items[i].name.clone()); | ||
} | ||
} | ||
|
||
_ => {} | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn initializes_correctly() { | ||
let menu = Menu::new("Title".to_string(), "Subtitle".to_string(), vec![]); | ||
assert_eq!(menu.selection_state.selected(), Some(0)); | ||
} | ||
|
||
#[test] | ||
fn navigates_correctly() { | ||
let mut menu = Menu::new( | ||
"Title".to_string(), | ||
"Subtitle".to_string(), | ||
vec![ | ||
"Option 1".to_string(), | ||
"Option 2".to_string(), | ||
"Option 3".to_string(), | ||
], | ||
); | ||
menu.up(); | ||
assert_eq!(menu.selection_state.selected(), Some(2)); | ||
|
||
menu.down(); | ||
assert_eq!(menu.selection_state.selected(), Some(0)); | ||
|
||
menu.down(); | ||
assert_eq!(menu.selection_state.selected(), Some(1)); | ||
} | ||
|
||
#[test] | ||
fn selects_item_correctly() { | ||
let mut menu = Menu::new( | ||
"Title".to_string(), | ||
"Subtitle".to_string(), | ||
vec!["Option 1".to_string()], | ||
); | ||
menu.toggle_selection(); | ||
|
||
assert_eq!(menu.items[0].selected, true); | ||
} | ||
} |
Oops, something went wrong.