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 = "generics2", path = "../exercises/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_sol", path = "../solutions/15_traits/traits1.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:
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
[[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};
mod scroll_state;
mod state;
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,
};
const MAX_SCROLL_PADDING: usize = 5;
use super::scroll_state::ScrollState;
// +1 for column padding.
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.
pub message: String,
app_state: &'a mut AppState,
scroll_state: ScrollState,
name_col_width: usize,
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_height: u16,
separator_line: Vec<u8>,
narrow_term: bool,
show_footer: bool,
max_n_rows_to_display: usize,
scroll_padding: usize,
}
impl<'a> ListState<'a> {
@ -70,50 +66,29 @@ impl<'a> ListState<'a> {
let n_rows_with_filter = app_state.exercises().len();
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 {
message: String::with_capacity(128),
app_state,
scroll_state,
name_col_width,
filter,
n_rows_with_filter,
selected_row: Some(selected),
row_offset: selected.saturating_sub(MAX_SCROLL_PADDING),
// Set by `set_term_size`
term_width: 0,
term_height: 0,
separator_line: Vec::new(),
narrow_term: false,
show_footer: true,
max_n_rows_to_display: 0,
scroll_padding: 0,
};
let (width, height) = terminal::size()?;
slf.set_term_size(width, height);
slf.draw(stdout)?;
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) {
self.term_width = width;
self.term_height = height;
@ -124,7 +99,7 @@ impl<'a> ListState<'a> {
let wide_help_footer_width = 95;
// 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;
// 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.max_n_rows_to_display = height
.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
as usize;
self.scroll_padding = (self.max_n_rows_to_display / 4).min(MAX_SCROLL_PADDING);
self.update_offset();
self.scroll_state.set_max_n_rows_to_display(
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
as usize,
);
}
fn draw_rows(
@ -150,15 +122,16 @@ impl<'a> ListState<'a> {
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
) -> io::Result<usize> {
let current_exercise_ind = self.app_state.current_exercise_ind();
let row_offset = self.scroll_state.offset();
let mut n_displayed_rows = 0;
for (exercise_ind, exercise) in filtered_exercises
.skip(self.row_offset)
.take(self.max_n_rows_to_display)
.skip(row_offset)
.take(self.scroll_state.max_n_rows_to_display())
{
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 {
r: 40,
g: 40,
@ -225,7 +198,7 @@ impl<'a> ListState<'a> {
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)?;
}
@ -247,7 +220,7 @@ impl<'a> ListState<'a> {
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
if self.message.is_empty() {
// 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")?;
if self.narrow_term {
next_ln(stdout)?;
@ -298,13 +271,8 @@ impl<'a> ListState<'a> {
stdout.queue(EndSynchronizedUpdate)?.flush()
}
fn set_selected(&mut self, selected: usize) {
self.selected_row = Some(selected);
self.update_offset();
}
fn update_rows(&mut self) {
self.n_rows_with_filter = match self.filter {
let n_rows = match self.filter {
Filter::Done => self
.app_state
.exercises()
@ -320,15 +288,7 @@ impl<'a> ListState<'a> {
Filter::None => self.app_state.exercises().len(),
};
if self.n_rows_with_filter == 0 {
self.selected_row = None;
return;
}
self.set_selected(
self.selected_row
.map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)),
);
self.scroll_state.set_n_rows(n_rows);
}
#[inline]
@ -341,28 +301,24 @@ impl<'a> ListState<'a> {
self.update_rows();
}
#[inline]
pub fn select_next(&mut self) {
if let Some(selected) = self.selected_row {
self.set_selected((selected + 1).min(self.n_rows_with_filter - 1));
}
self.scroll_state.select_next();
}
#[inline]
pub fn select_previous(&mut self) {
if let Some(selected) = self.selected_row {
self.set_selected(selected.saturating_sub(1));
}
self.scroll_state.select_previous();
}
#[inline]
pub fn select_first(&mut self) {
if self.n_rows_with_filter > 0 {
self.set_selected(0);
}
self.scroll_state.select_first();
}
#[inline]
pub fn select_last(&mut self) {
if self.n_rows_with_filter > 0 {
self.set_selected(self.n_rows_with_filter - 1);
}
self.scroll_state.select_last();
}
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<()> {
let Some(selected) = self.selected_row else {
let Some(selected) = self.scroll_state.selected() else {
self.message.push_str("Nothing selected to reset!");
return Ok(());
};
@ -408,7 +364,7 @@ impl<'a> ListState<'a> {
// Return `true` if there was something to select.
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!");
return Ok(false);
};

View file

@ -71,9 +71,7 @@ fn main() -> Result<()> {
}
match args.command {
Some(Subcommands::Init) => {
return init::init().context("Initialization failed");
}
Some(Subcommands::Init) => return init::init().context("Initialization failed"),
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<()> {
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")?;
Ok(())
stdout.write_all(b"\n")
}
pub fn terminal_file_link<'a>(