Skip to content

Commit

Permalink
Merge pull request #29 from warpy-ai/28-featue-create-a-simple-menu-list
Browse files Browse the repository at this point in the history
28 featue create a simple menu list
  • Loading branch information
jucasoliveira authored May 7, 2024
2 parents 0c3b0e7 + 4d8bc0c commit 0fcfe38
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
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."
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project aims to provide a set of components that can be used in your termin
- [Progress bar Component](#progress-bar-component)
- [Timer Component](#timer-component)
- [Stopwatch Component](#stopwatch-component)
- [Viewport Component](#viewport-component)
- [List Component](#list-component)
- [MenuList Component](#menulist-component)

# TextInput Component

Expand Down Expand Up @@ -167,6 +170,16 @@ A list component, build with ratatui.

- [Example Code](https://github.com/warpy-ai/rustubble/blob/main/examples/list_example.rs)

# MenuList Component

![menulist](https://github.com/warpy-ai/rustubble/blob/main/assets/menulist.gif)

A menu list component, build with ratatui.

## Usage

- [Example Code](https://github.com/warpy-ai/rustubble/blob/main/examples/menu_list_example.rs)

## Contribution

Contributions are welcome! If you have suggestions for improving the spinner or adding new styles, please open an issue or pull request on our GitHub repository.
Expand Down
Binary file added assets/menulist.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 25 additions & 1 deletion examples/list_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,31 @@ fn main() -> Result<(), io::Error> {
Item {
title: "Bicoin".to_string(),
subtitle: "Cheap".to_string(),
}, // Add more items
},
Item {
title: "Coke".to_string(),
subtitle: "Cheap".to_string(),
},
Item {
title: "Sprite".to_string(),
subtitle: "Cheap".to_string(),
},
Item {
title: "Sprite".to_string(),
subtitle: "Cheap".to_string(),
},
Item {
title: "Sprite".to_string(),
subtitle: "Cheap".to_string(),
},
Item {
title: "Sprite".to_string(),
subtitle: "Cheap".to_string(),
},
Item {
title: "Sprite".to_string(),
subtitle: "Cheap".to_string(),
},
],
);

Expand Down
25 changes: 25 additions & 0 deletions examples/menu_list_example.rs
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()
}
6 changes: 3 additions & 3 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ impl Command {
Command::Delete => "del",
Command::Help => "h",
Command::ControlC => "cntrl+c",
Command::Enter => "\u{2B90}",
Command::Enter => "\u{2B90} ",
Command::Filter => "/",
Command::Up => "\u{2191}/h",
Command::Down => "\u{2193}/l",
Command::Up => "\u{2191}/k",
Command::Down => "\u{2193}/j",
// Additional commands
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod help;
pub mod helper;
pub mod input;
pub mod list;
pub mod menu_list;
pub mod progress_bar;
pub mod spinner;
pub mod stopwatch;
Expand Down
6 changes: 3 additions & 3 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use ratatui::{
};

use crate::{
command::{self, CommandInfo},
command::{CommandInfo},
help::HelpComponent,
};

Expand Down Expand Up @@ -152,7 +152,7 @@ impl ItemList {
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3),
Constraint::Length(1),
Constraint::Percentage(50),
Constraint::Length(3),
]
Expand Down Expand Up @@ -249,7 +249,7 @@ pub fn handle_list(list: &mut ItemList, x: u16, y: u16) -> Option<String> {
KeyCode::Char('/') => {
list.showing_filter = !list.showing_filter;
}
KeyCode::Esc => list.showing_filter = !list.showing_filter,
KeyCode::Esc => list.showing_filter = false,
KeyCode::Char('q') => return None,
KeyCode::Down => list.next(),
KeyCode::Up => list.previous(),
Expand Down
256 changes: 256 additions & 0 deletions src/menu_list.rs
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);
}
}
Loading

0 comments on commit 0fcfe38

Please sign in to comment.