Compare commits

...

8 commits

Author SHA1 Message Date
Kacper Poneta 45099dc919
Merge 59e8f70e55 into dd52e9cd72 2024-08-27 18:01:03 +02:00
mo8it dd52e9cd72 Separate the scroll state 2024-08-27 00:03:50 +02:00
mo8it 0f71a150ff Making code prettier :P 2024-08-26 22:03:09 +02:00
mo8it 59e8f70e55 Format code 2024-07-12 18:31:23 +02:00
mo8it 4c8365fe88 Update dev/Cargo.toml 2024-07-12 18:25:01 +02:00
Kacper Poneta 52af0674c1 changed the task to make it more appropriate 2024-07-12 18:14:40 +02:00
Kacper Poneta 938b90e5f2 very small solution update 2024-07-11 22:55:48 +02:00
Kacper Poneta 55cc8584bd added exercise 2024-07-11 22:53:38 +02:00
9 changed files with 257 additions and 79 deletions

View file

@ -116,6 +116,8 @@ bin = [
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" }, { name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" }, { name = "generics2", path = "../exercises/14_generics/generics2.rs" },
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" }, { name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
{ name = "generics3", path = "../exercises/14_generics/generics3.rs" },
{ name = "generics3_sol", path = "../solutions/14_generics/generics3.rs" },
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" }, { name = "traits1", path = "../exercises/15_traits/traits1.rs" },
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" }, { name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" }, { name = "traits2", path = "../exercises/15_traits/traits2.rs" },

View file

@ -0,0 +1,54 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// This function should take an array of `Option` elements and returns array of not None elements
// TODO fix this function signature
fn into_dispose_nulls(list: Vec<Option<&str>>) -> Vec<&str> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
#[allow(dead_code)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -744,6 +744,17 @@ hint = """
Related section in The Book: Related section in The Book:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions""" https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions"""
[[exercises]]
name = "generics3"
dir = "14_generics"
hint = """
Vectors in Rust use generics to create dynamically-sized arrays of any type.
The `into_dispose_nulls` function takes a vector as an argument, but only accepts vectors that store the &str type.
To allow the function to accept vectors that store any type, you can leverage your knowledge about generics.
If you're unsure how to proceed, please refer to the Rust Book at:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-function-definitions.
"""
# TRAITS # TRAITS
[[exercises]] [[exercises]]

View file

@ -0,0 +1,53 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// Here we added generic type `T` to function signature
// Now this function can be used with vector of any
fn into_dispose_nulls<T>(list: Vec<Option<T>>) -> Vec<T> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -16,6 +16,7 @@ use crate::app_state::AppState;
use self::state::{Filter, ListState}; use self::state::{Filter, ListState};
mod scroll_state;
mod state; mod state;
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {

104
src/list/scroll_state.rs Normal file
View file

@ -0,0 +1,104 @@
pub struct ScrollState {
n_rows: usize,
max_n_rows_to_display: usize,
selected: Option<usize>,
offset: usize,
scroll_padding: usize,
max_scroll_padding: usize,
}
impl ScrollState {
pub fn new(n_rows: usize, selected: Option<usize>, max_scroll_padding: usize) -> Self {
Self {
n_rows,
max_n_rows_to_display: 0,
selected,
offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)),
scroll_padding: 0,
max_scroll_padding,
}
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
fn update_offset(&mut self) {
let Some(selected) = self.selected else {
return;
};
let min_offset = (selected + self.scroll_padding)
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
let max_offset = selected.saturating_sub(self.scroll_padding);
let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display);
self.offset = self
.offset
.max(min_offset)
.min(max_offset)
.min(global_max_offset);
}
#[inline]
pub fn selected(&self) -> Option<usize> {
self.selected
}
fn set_selected(&mut self, selected: usize) {
self.selected = Some(selected);
self.update_offset();
}
pub fn select_next(&mut self) {
if let Some(selected) = self.selected {
self.set_selected((selected + 1).min(self.n_rows - 1));
}
}
pub fn select_previous(&mut self) {
if let Some(selected) = self.selected {
self.set_selected(selected.saturating_sub(1));
}
}
pub fn select_first(&mut self) {
if self.n_rows > 0 {
self.set_selected(0);
}
}
pub fn select_last(&mut self) {
if self.n_rows > 0 {
self.set_selected(self.n_rows - 1);
}
}
pub fn set_n_rows(&mut self, n_rows: usize) {
self.n_rows = n_rows;
if self.n_rows == 0 {
self.selected = None;
return;
}
self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1)));
}
#[inline]
fn update_scroll_padding(&mut self) {
self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding);
}
#[inline]
pub fn max_n_rows_to_display(&self) -> usize {
self.max_n_rows_to_display
}
pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) {
self.max_n_rows_to_display = max_n_rows_to_display;
self.update_scroll_padding();
self.update_offset();
}
}

View file

@ -17,7 +17,8 @@ use crate::{
MAX_EXERCISE_NAME_LEN, MAX_EXERCISE_NAME_LEN,
}; };
const MAX_SCROLL_PADDING: usize = 5; use super::scroll_state::ScrollState;
// +1 for column padding. // +1 for column padding.
const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1]; const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1];
@ -39,19 +40,14 @@ pub struct ListState<'a> {
/// Footer message to be displayed if not empty. /// Footer message to be displayed if not empty.
pub message: String, pub message: String,
app_state: &'a mut AppState, app_state: &'a mut AppState,
scroll_state: ScrollState,
name_col_width: usize, name_col_width: usize,
filter: Filter, filter: Filter,
n_rows_with_filter: usize,
/// Selected row out of the filtered ones.
selected_row: Option<usize>,
row_offset: usize,
term_width: u16, term_width: u16,
term_height: u16, term_height: u16,
separator_line: Vec<u8>, separator_line: Vec<u8>,
narrow_term: bool, narrow_term: bool,
show_footer: bool, show_footer: bool,
max_n_rows_to_display: usize,
scroll_padding: usize,
} }
impl<'a> ListState<'a> { impl<'a> ListState<'a> {
@ -70,50 +66,29 @@ impl<'a> ListState<'a> {
let n_rows_with_filter = app_state.exercises().len(); let n_rows_with_filter = app_state.exercises().len();
let selected = app_state.current_exercise_ind(); let selected = app_state.current_exercise_ind();
let (width, height) = terminal::size()?;
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
let mut slf = Self { let mut slf = Self {
message: String::with_capacity(128), message: String::with_capacity(128),
app_state, app_state,
scroll_state,
name_col_width, name_col_width,
filter, filter,
n_rows_with_filter,
selected_row: Some(selected),
row_offset: selected.saturating_sub(MAX_SCROLL_PADDING),
// Set by `set_term_size` // Set by `set_term_size`
term_width: 0, term_width: 0,
term_height: 0, term_height: 0,
separator_line: Vec::new(), separator_line: Vec::new(),
narrow_term: false, narrow_term: false,
show_footer: true, show_footer: true,
max_n_rows_to_display: 0,
scroll_padding: 0,
}; };
let (width, height) = terminal::size()?;
slf.set_term_size(width, height); slf.set_term_size(width, height);
slf.draw(stdout)?; slf.draw(stdout)?;
Ok(slf) Ok(slf)
} }
fn update_offset(&mut self) {
let Some(selected) = self.selected_row else {
return;
};
let min_offset = (selected + self.scroll_padding)
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
let max_offset = selected.saturating_sub(self.scroll_padding);
let global_max_offset = self
.n_rows_with_filter
.saturating_sub(self.max_n_rows_to_display);
self.row_offset = self
.row_offset
.max(min_offset)
.min(max_offset)
.min(global_max_offset);
}
pub fn set_term_size(&mut self, width: u16, height: u16) { pub fn set_term_size(&mut self, width: u16, height: u16) {
self.term_width = width; self.term_width = width;
self.term_height = height; self.term_height = height;
@ -124,7 +99,7 @@ impl<'a> ListState<'a> {
let wide_help_footer_width = 95; let wide_help_footer_width = 95;
// The help footer is shorter when nothing is selected. // The help footer is shorter when nothing is selected.
self.narrow_term = width < wide_help_footer_width && self.selected_row.is_some(); self.narrow_term = width < wide_help_footer_width && self.scroll_state.selected().is_some();
let header_height = 1; let header_height = 1;
// 2 separator, 1 progress bar, 1-2 footer message. // 2 separator, 1 progress bar, 1-2 footer message.
@ -135,13 +110,10 @@ impl<'a> ListState<'a> {
self.separator_line = "".as_bytes().repeat(width as usize); self.separator_line = "".as_bytes().repeat(width as usize);
} }
self.max_n_rows_to_display = height self.scroll_state.set_max_n_rows_to_display(
.saturating_sub(header_height + u16::from(self.show_footer) * footer_height) height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
as usize; as usize,
);
self.scroll_padding = (self.max_n_rows_to_display / 4).min(MAX_SCROLL_PADDING);
self.update_offset();
} }
fn draw_rows( fn draw_rows(
@ -150,15 +122,16 @@ impl<'a> ListState<'a> {
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>, filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
) -> io::Result<usize> { ) -> io::Result<usize> {
let current_exercise_ind = self.app_state.current_exercise_ind(); let current_exercise_ind = self.app_state.current_exercise_ind();
let row_offset = self.scroll_state.offset();
let mut n_displayed_rows = 0; let mut n_displayed_rows = 0;
for (exercise_ind, exercise) in filtered_exercises for (exercise_ind, exercise) in filtered_exercises
.skip(self.row_offset) .skip(row_offset)
.take(self.max_n_rows_to_display) .take(self.scroll_state.max_n_rows_to_display())
{ {
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.selected_row == Some(self.row_offset + n_displayed_rows) { if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
writer.stdout.queue(SetBackgroundColor(Color::Rgb { writer.stdout.queue(SetBackgroundColor(Color::Rgb {
r: 40, r: 40,
g: 40, g: 40,
@ -225,7 +198,7 @@ impl<'a> ListState<'a> {
Filter::None => self.draw_rows(stdout, iter)?, Filter::None => self.draw_rows(stdout, iter)?,
}; };
for _ in 0..self.max_n_rows_to_display - n_displayed_rows { for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows {
next_ln(stdout)?; next_ln(stdout)?;
} }
@ -247,7 +220,7 @@ impl<'a> ListState<'a> {
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.message.is_empty() { if self.message.is_empty() {
// Help footer message // Help footer message
if self.selected_row.is_some() { if self.scroll_state.selected().is_some() {
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?; writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
if self.narrow_term { if self.narrow_term {
next_ln(stdout)?; next_ln(stdout)?;
@ -298,13 +271,8 @@ impl<'a> ListState<'a> {
stdout.queue(EndSynchronizedUpdate)?.flush() stdout.queue(EndSynchronizedUpdate)?.flush()
} }
fn set_selected(&mut self, selected: usize) {
self.selected_row = Some(selected);
self.update_offset();
}
fn update_rows(&mut self) { fn update_rows(&mut self) {
self.n_rows_with_filter = match self.filter { let n_rows = match self.filter {
Filter::Done => self Filter::Done => self
.app_state .app_state
.exercises() .exercises()
@ -320,15 +288,7 @@ impl<'a> ListState<'a> {
Filter::None => self.app_state.exercises().len(), Filter::None => self.app_state.exercises().len(),
}; };
if self.n_rows_with_filter == 0 { self.scroll_state.set_n_rows(n_rows);
self.selected_row = None;
return;
}
self.set_selected(
self.selected_row
.map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)),
);
} }
#[inline] #[inline]
@ -341,28 +301,24 @@ impl<'a> ListState<'a> {
self.update_rows(); self.update_rows();
} }
#[inline]
pub fn select_next(&mut self) { pub fn select_next(&mut self) {
if let Some(selected) = self.selected_row { self.scroll_state.select_next();
self.set_selected((selected + 1).min(self.n_rows_with_filter - 1));
}
} }
#[inline]
pub fn select_previous(&mut self) { pub fn select_previous(&mut self) {
if let Some(selected) = self.selected_row { self.scroll_state.select_previous();
self.set_selected(selected.saturating_sub(1));
}
} }
#[inline]
pub fn select_first(&mut self) { pub fn select_first(&mut self) {
if self.n_rows_with_filter > 0 { self.scroll_state.select_first();
self.set_selected(0);
}
} }
#[inline]
pub fn select_last(&mut self) { pub fn select_last(&mut self) {
if self.n_rows_with_filter > 0 { self.scroll_state.select_last();
self.set_selected(self.n_rows_with_filter - 1);
}
} }
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> { fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
@ -390,7 +346,7 @@ impl<'a> ListState<'a> {
} }
pub fn reset_selected(&mut self) -> Result<()> { pub fn reset_selected(&mut self) -> Result<()> {
let Some(selected) = self.selected_row else { let Some(selected) = self.scroll_state.selected() else {
self.message.push_str("Nothing selected to reset!"); self.message.push_str("Nothing selected to reset!");
return Ok(()); return Ok(());
}; };
@ -408,7 +364,7 @@ impl<'a> ListState<'a> {
// Return `true` if there was something to select. // Return `true` if there was something to select.
pub fn selected_to_current_exercise(&mut self) -> Result<bool> { pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
let Some(selected) = self.selected_row else { let Some(selected) = self.scroll_state.selected() else {
self.message.push_str("Nothing selected to continue at!"); self.message.push_str("Nothing selected to continue at!");
return Ok(false); return Ok(false);
}; };

View file

@ -71,9 +71,7 @@ fn main() -> Result<()> {
} }
match args.command { match args.command {
Some(Subcommands::Init) => { Some(Subcommands::Init) => return init::init().context("Initialization failed"),
return init::init().context("Initialization failed");
}
Some(Subcommands::Dev(dev_command)) => return dev_command.run(), Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
_ => (), _ => (),
} }

View file

@ -153,8 +153,7 @@ pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> { pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?; stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?; io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")?; stdout.write_all(b"\n")
Ok(())
} }
pub fn terminal_file_link<'a>( pub fn terminal_file_link<'a>(