move ui helpers to own module

This commit is contained in:
Crispy 2024-12-22 13:03:12 +01:00
parent da12e8519e
commit 12b39467e3
5 changed files with 440 additions and 442 deletions

View file

@ -9,19 +9,13 @@ use raylib::prelude::*;
use crate::{
blueprint::Blueprint,
draw_scaled_texture, draw_usize, get_free_id, get_scroll,
level::Level,
marble_engine::{
board::{Board, ResizeDeltas},
pos::{Pos, PosInt},
tile::{Claim, Comparison, Direction, MathOp, MirrorType, OpenTile, PTile, Tile, WireType},
Machine,
},
simple_button, simple_option_button, simple_toggle_button, slider,
solution::{Score, Solution},
text_input, texture_option_button,
marble_engine::{board::*, pos::*, tile::*, Machine},
solution::*,
theme::*,
userdata_dir, Scroll, Textures, Tooltip, TILE_TEXTURE_SIZE,
ui::*,
util::*,
TILE_TEXTURE_SIZE,
};
const HEADER_HEIGHT: i32 = 40;

View file

@ -12,11 +12,13 @@ mod marble_engine;
mod solution;
mod theme;
mod util;
mod ui;
use editor::{Editor, ExitState};
use level::Level;
use solution::Solution;
use theme::*;
use ui::{simple_button, simple_option_button, text_input, ShapedText};
use util::*;
const TITLE_TEXT: &str = concat!("Marble Machinations v", env!("CARGO_PKG_VERSION"));

View file

@ -7,7 +7,7 @@ use board::Board;
use pos::*;
use tile::*;
use crate::{draw_usize_small, Textures, TILE_TEXTURE_SIZE};
use crate::{ ui::draw_usize_small, Textures, TILE_TEXTURE_SIZE};
#[derive(Debug)]
pub struct Machine {

431
src/ui.rs Normal file
View file

@ -0,0 +1,431 @@
use std::ops::Range;
use crate::{draw_scaled_texture, theme::*, Textures};
use raylib::prelude::*;
pub fn simple_button(d: &mut RaylibDrawHandle, x: i32, y: i32, width: i32, height: i32) -> bool {
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle {
x: x as f32,
y: y as f32,
width: width as f32,
height: height as f32,
};
let hover = bounds.check_collision_point_rec(mouse_pos);
let pressed = hover && d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT);
d.draw_rectangle(x, y, width, height, widget_bg(hover));
pressed
}
#[derive(Debug)]
pub struct ShapedText {
text: String,
max_width: i32,
font_size: i32,
lines: Vec<Range<usize>>,
}
impl ShapedText {
pub fn new(font_size: i32) -> Self {
Self {
text: String::new(),
font_size,
max_width: 500,
lines: Vec::new(),
}
}
pub fn set_text(&mut self, text: &str) {
if text != self.text {
self.text = text.to_owned();
self.lines.clear();
}
}
pub fn height(&self) -> i32 {
self.font_size * self.lines.len() as i32
}
pub fn update_width(&mut self, d: &RaylibHandle, width: i32) {
if self.max_width == width && !self.lines.is_empty() {
return;
}
self.max_width = width;
self.lines.clear();
// todo remove leading space on broken lines
// todo fix splitting very long words
let mut line_start = 0;
let mut line_end = 0;
for (i, c) in self.text.char_indices() {
if c == ' ' {
let line = &self.text[line_start..i];
let new_line_length = d.measure_text(line, self.font_size);
if new_line_length <= self.max_width {
line_end = i;
} else {
self.lines.push(line_start..line_end);
line_start = line_end;
}
}
if c == '\n' {
let line = &self.text[line_start..i];
let new_line_length = d.measure_text(line, self.font_size);
if new_line_length <= self.max_width {
self.lines.push(line_start..i);
line_end = i + 1;
line_start = i + 1;
} else {
self.lines.push(line_start..line_end);
self.lines.push(line_end..(i + 1));
line_start = i + 1;
line_end = i + 1;
}
}
}
let i = self.text.len();
let line = &self.text[line_start..i];
let new_line_length = d.measure_text(line, self.font_size);
if new_line_length <= self.max_width {
self.lines.push(line_start..i);
} else {
self.lines.push(line_start..line_end);
self.lines.push(line_end..i);
}
}
pub fn draw(&self, d: &mut RaylibDrawHandle, x: i32, y: i32) {
// d.draw_rectangle(x, y, self.max_width, 4, Color::RED);
for (i, line) in self.lines.iter().enumerate() {
let line = &self.text[line.clone()];
let line_y = y + self.font_size * i as i32;
d.draw_text(line, x, line_y, self.font_size, Color::WHITE);
}
}
}
#[derive(Debug, Default)]
pub struct Tooltip {
text: Option<&'static str>,
mouse_x: i32,
mouse_y: i32,
screen_width: i32,
screen_height: i32,
}
impl Tooltip {
pub fn init_frame(&mut self, d: &mut RaylibDrawHandle) {
let p = d.get_mouse_position();
*self = Self {
text: None,
mouse_x: p.x as i32,
mouse_y: p.y as i32,
screen_width: d.get_screen_width(),
screen_height: d.get_screen_height(),
};
}
pub fn add(&mut self, x: i32, y: i32, width: i32, height: i32, text: &'static str) {
if self.mouse_x >= x
&& self.mouse_y >= y
&& self.mouse_x <= (x + width)
&& self.mouse_y <= (y + height)
{
self.text = Some(text);
}
}
pub fn add_rec(&mut self, bounds: Rectangle, text: &'static str) {
self.add(
bounds.x as i32,
bounds.y as i32,
bounds.width as i32,
bounds.height as i32,
text,
);
}
pub fn draw(&self, d: &mut RaylibDrawHandle) {
if let Some(text) = self.text {
let font_size = 20;
let margin = 4;
let text_width = d.measure_text(text, font_size);
let x = self
.mouse_x
.min(self.screen_width - text_width - margin * 2);
let y = self
.mouse_y
.min(self.screen_height - font_size - margin * 2);
d.draw_rectangle(
x,
y,
text_width + margin * 2,
font_size + margin * 2,
BG_LIGHT,
);
d.draw_text(text, x + margin, y + margin, font_size, Color::WHITE);
}
}
}
pub fn simple_toggle_button(
d: &mut RaylibDrawHandle,
state: &mut bool,
x: i32,
y: i32,
width: i32,
height: i32,
margin: i32,
) {
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle {
x: x as f32,
y: y as f32,
width: width as f32,
height: height as f32,
};
let hover = bounds.check_collision_point_rec(mouse_pos);
d.draw_rectangle(x, y, width, height, widget_bg(hover));
if *state {
d.draw_rectangle(
x + margin,
y + margin,
width - margin * 2,
height - margin * 2,
FG_TOGGLE_ENABLED,
);
}
if hover && d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT) {
*state = !*state;
}
}
pub fn simple_option_button<T>(
d: &mut RaylibDrawHandle,
x: i32,
y: i32,
width: i32,
height: i32,
option: T,
current: &mut T,
) -> bool
where
T: PartialEq,
{
let bounds = Rectangle::new(x as f32, y as f32, width as f32, height as f32);
d.draw_rectangle_rec(bounds, widget_bg(&option == current));
let mouse_pos = d.get_mouse_position();
let mut changed = false;
if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& bounds.check_collision_point_rec(mouse_pos)
&& current != &option
{
*current = option;
changed = true;
}
changed
}
pub fn text_input(
d: &mut RaylibDrawHandle,
bounds: Rectangle,
text: &mut String,
is_selected: &mut bool,
max_len: usize,
editable: bool,
) -> bool {
let mut changed = false;
d.draw_rectangle_rec(bounds, widget_bg(*is_selected));
d.draw_rectangle_rec(
Rectangle::new(
bounds.x + 2.,
bounds.y + bounds.height - 5.,
bounds.width - 4.,
3.,
),
BG_DARK,
);
let drawn_text = if *is_selected {
&format!("{text}_")
} else {
text.as_str()
};
d.draw_text(
drawn_text,
bounds.x as i32 + 4,
bounds.y as i32 + 4,
20,
Color::WHITE,
);
let mouse_pos = d.get_mouse_position();
if editable
&& d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& (bounds.check_collision_point_rec(mouse_pos) || *is_selected)
{
*is_selected = !*is_selected;
}
if *is_selected {
if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
*is_selected = false;
}
if d.is_key_pressed(KeyboardKey::KEY_BACKSPACE) && !text.is_empty() {
changed = true;
text.pop();
}
if text.len() < max_len {
let char_code = unsafe { ffi::GetCharPressed() };
let c = if char_code > 0 {
char::from_u32(char_code as u32)
} else {
None
};
if let Some(c) = c {
changed = true;
text.push(c);
}
}
}
changed
}
pub fn texture_option_button<T>(
d: &mut RaylibDrawHandle,
pos: Vector2,
texture: &Texture2D,
option: T,
current: &mut T,
tex_size: f32,
border: f32,
// tooltip
) where
T: PartialEq,
{
let bounds = Rectangle {
x: pos.x,
y: pos.y,
width: tex_size + border * 2.,
height: tex_size + border * 2.,
};
d.draw_rectangle_rec(
bounds,
if &option == current {
BG_WIDGET_ACTIVE
} else {
gray(16)
},
);
d.draw_texture_ex(
texture,
pos + Vector2::new(border, border),
0.,
tex_size / texture.width as f32,
Color::WHITE,
);
let mouse_pos = d.get_mouse_position();
if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& bounds.check_collision_point_rec(mouse_pos)
{
*current = option;
}
}
pub fn draw_usize(
d: &mut RaylibDrawHandle,
textures: &Textures,
number: usize,
x: i32,
y: i32,
digits: u8,
scale: u8,
) {
let digits = digits as i32;
let scale = scale as i32;
for i in 0..digits {
d.draw_rectangle(x + 10 * i * scale, y, 8 * scale, 16 * scale, BG_LIGHT);
}
let mut num = number;
let mut i = 0;
while (num != 0 || i == 0) && i < digits {
let texture = textures.get(&format!("digit_{}", num % 10));
let x = x + (digits - i - 1) * 10 * scale;
draw_scaled_texture(d, texture, x, y, scale as f32);
num /= 10;
i += 1;
}
}
pub fn draw_usize_small(
d: &mut RaylibDrawHandle,
textures: &Textures,
mut num: usize,
mut x: i32,
y: i32,
scale: f32,
) {
const MAX_DIGITS: usize = 8;
let mut digits = [0; MAX_DIGITS];
let mut i = 0;
while (num != 0 || i == 0) && i < MAX_DIGITS {
digits[MAX_DIGITS - i - 1] = num % 10;
num /= 10;
i += 1;
}
let texture = textures.get("digits_small");
for &digit in &digits[(MAX_DIGITS - i)..] {
let source = Rectangle::new(4. * digit as f32, 0., 4., 6.);
let dest = Rectangle::new(x as f32, y as f32, 4. * scale, 6. * scale);
d.draw_texture_pro(texture, source, dest, Vector2::zero(), 0., FG_MARBLE_VALUE);
x += 4 * scale as i32;
}
}
pub fn slider(
d: &mut RaylibDrawHandle,
value: &mut u8,
min: u8,
max: u8,
x: i32,
y: i32,
width: i32,
height: i32,
) -> bool {
// the +1 makes the lowest state look slightly filled and the max state fully filled
let percent = (*value - min + 1) as f32 / (max - min + 1) as f32;
d.draw_rectangle(x, y, width, height, BG_WIDGET);
d.draw_rectangle(x, y, (width as f32 * percent) as i32, height, Color::CYAN);
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle::new(x as f32, y as f32, width as f32, height as f32);
if bounds.check_collision_point_rec(mouse_pos) {
if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) {
let percent = (mouse_pos.x - bounds.x) / bounds.width;
let new_value = min + (percent * (max - min + 1) as f32) as u8;
if *value != new_value {
*value = new_value;
}
} else if d.get_mouse_wheel_move() > 0.5 && *value < max {
*value += 1;
} else if d.get_mouse_wheel_move() < -0.5 && *value > min {
*value -= 1;
}
}
false
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Scroll {
Up,
Down,
}
pub fn get_scroll(rl: &RaylibHandle) -> Option<Scroll> {
const SCROLL_THRESHOLD: f32 = 0.5;
let value = rl.get_mouse_wheel_move();
if value > SCROLL_THRESHOLD {
Some(Scroll::Up)
} else if value < -SCROLL_THRESHOLD {
Some(Scroll::Down)
} else {
None
}
}

View file

@ -1,9 +1,7 @@
use std::{collections::HashMap, fs::read_dir, ops::Range, path::PathBuf};
use std::{collections::HashMap, fs::read_dir, path::PathBuf};
use raylib::prelude::*;
use crate::theme::*;
#[derive(Default)]
pub struct Textures {
map: HashMap<String, Texture2D>,
@ -30,415 +28,6 @@ impl Textures {
}
}
pub fn simple_button(d: &mut RaylibDrawHandle, x: i32, y: i32, width: i32, height: i32) -> bool {
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle {
x: x as f32,
y: y as f32,
width: width as f32,
height: height as f32,
};
let hover = bounds.check_collision_point_rec(mouse_pos);
let pressed = hover && d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT);
d.draw_rectangle(x, y, width, height, widget_bg(hover));
pressed
}
#[derive(Debug)]
pub struct ShapedText {
text: String,
max_width: i32,
font_size: i32,
lines: Vec<Range<usize>>,
}
impl ShapedText {
pub fn new(font_size: i32) -> Self {
Self {
text: String::new(),
font_size,
max_width: 500,
lines: Vec::new(),
}
}
pub fn set_text(&mut self, text: &str) {
if text != self.text {
self.text = text.to_owned();
self.lines.clear();
}
}
pub fn height(&self) -> i32 {
self.font_size * self.lines.len() as i32
}
pub fn update_width(&mut self, d: &RaylibHandle, width: i32) {
if self.max_width == width && !self.lines.is_empty() {
return;
}
self.max_width = width;
self.lines.clear();
// todo remove leading space on broken lines
// todo fix splitting very long words
let mut line_start = 0;
let mut line_end = 0;
for (i, c) in self.text.char_indices() {
if c == ' ' {
let line = &self.text[line_start..i];
let new_line_length = d.measure_text(line, self.font_size);
if new_line_length <= self.max_width {
line_end = i;
} else {
self.lines.push(line_start..line_end);
line_start = line_end;
}
}
if c == '\n' {
let line = &self.text[line_start..i];
let new_line_length = d.measure_text(line, self.font_size);
if new_line_length <= self.max_width {
self.lines.push(line_start..i);
line_end = i + 1;
line_start = i + 1;
} else {
self.lines.push(line_start..line_end);
self.lines.push(line_end..(i + 1));
line_start = i + 1;
line_end = i + 1;
}
}
}
let i = self.text.len();
let line = &self.text[line_start..i];
let new_line_length = d.measure_text(line, self.font_size);
if new_line_length <= self.max_width {
self.lines.push(line_start..i);
} else {
self.lines.push(line_start..line_end);
self.lines.push(line_end..i);
}
}
pub fn draw(&self, d: &mut RaylibDrawHandle, x: i32, y: i32) {
// d.draw_rectangle(x, y, self.max_width, 4, Color::RED);
for (i, line) in self.lines.iter().enumerate() {
let line = &self.text[line.clone()];
let line_y = y + self.font_size * i as i32;
d.draw_text(line, x, line_y, self.font_size, Color::WHITE);
}
}
}
#[derive(Debug, Default)]
pub struct Tooltip {
text: Option<&'static str>,
mouse_x: i32,
mouse_y: i32,
screen_width: i32,
screen_height: i32,
}
impl Tooltip {
pub fn init_frame(&mut self, d: &mut RaylibDrawHandle) {
let p = d.get_mouse_position();
*self = Self {
text: None,
mouse_x: p.x as i32,
mouse_y: p.y as i32,
screen_width: d.get_screen_width(),
screen_height: d.get_screen_height(),
};
}
pub fn add(&mut self, x: i32, y: i32, width: i32, height: i32, text: &'static str) {
if self.mouse_x >= x
&& self.mouse_y >= y
&& self.mouse_x <= (x + width)
&& self.mouse_y <= (y + height)
{
self.text = Some(text);
}
}
pub fn add_rec(&mut self, bounds: Rectangle, text: &'static str) {
self.add(
bounds.x as i32,
bounds.y as i32,
bounds.width as i32,
bounds.height as i32,
text,
);
}
pub fn draw(&self, d: &mut RaylibDrawHandle) {
if let Some(text) = self.text {
let font_size = 20;
let margin = 4;
let text_width = d.measure_text(text, font_size);
let x = self
.mouse_x
.min(self.screen_width - text_width - margin * 2);
let y = self
.mouse_y
.min(self.screen_height - font_size - margin * 2);
d.draw_rectangle(
x,
y,
text_width + margin * 2,
font_size + margin * 2,
BG_LIGHT,
);
d.draw_text(text, x + margin, y + margin, font_size, Color::WHITE);
}
}
}
pub fn simple_toggle_button(
d: &mut RaylibDrawHandle,
state: &mut bool,
x: i32,
y: i32,
width: i32,
height: i32,
margin: i32,
) {
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle {
x: x as f32,
y: y as f32,
width: width as f32,
height: height as f32,
};
let hover = bounds.check_collision_point_rec(mouse_pos);
d.draw_rectangle(x, y, width, height, widget_bg(hover));
if *state {
d.draw_rectangle(
x + margin,
y + margin,
width - margin * 2,
height - margin * 2,
FG_TOGGLE_ENABLED,
);
}
if hover && d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT) {
*state = !*state;
}
}
pub fn simple_option_button<T>(
d: &mut RaylibDrawHandle,
x: i32,
y: i32,
width: i32,
height: i32,
option: T,
current: &mut T,
) -> bool
where
T: PartialEq,
{
let bounds = Rectangle::new(x as f32, y as f32, width as f32, height as f32);
d.draw_rectangle_rec(bounds, widget_bg(&option == current));
let mouse_pos = d.get_mouse_position();
let mut changed = false;
if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& bounds.check_collision_point_rec(mouse_pos)
&& current != &option
{
*current = option;
changed = true;
}
changed
}
pub fn text_input(
d: &mut RaylibDrawHandle,
bounds: Rectangle,
text: &mut String,
is_selected: &mut bool,
max_len: usize,
editable: bool,
) -> bool {
let mut changed = false;
d.draw_rectangle_rec(bounds, widget_bg(*is_selected));
d.draw_rectangle_rec(
Rectangle::new(
bounds.x + 2.,
bounds.y + bounds.height - 5.,
bounds.width - 4.,
3.,
),
BG_DARK,
);
let drawn_text = if *is_selected {
&format!("{text}_")
} else {
text.as_str()
};
d.draw_text(
drawn_text,
bounds.x as i32 + 4,
bounds.y as i32 + 4,
20,
Color::WHITE,
);
let mouse_pos = d.get_mouse_position();
if editable
&& d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& (bounds.check_collision_point_rec(mouse_pos) || *is_selected)
{
*is_selected = !*is_selected;
}
if *is_selected {
if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
*is_selected = false;
}
if d.is_key_pressed(KeyboardKey::KEY_BACKSPACE) && !text.is_empty() {
changed = true;
text.pop();
}
if text.len() < max_len {
let char_code = unsafe { ffi::GetCharPressed() };
let c = if char_code > 0 {
char::from_u32(char_code as u32)
} else {
None
};
if let Some(c) = c {
changed = true;
text.push(c);
}
}
}
changed
}
pub fn texture_option_button<T>(
d: &mut RaylibDrawHandle,
pos: Vector2,
texture: &Texture2D,
option: T,
current: &mut T,
tex_size: f32,
border: f32,
// tooltip
) where
T: PartialEq,
{
let bounds = Rectangle {
x: pos.x,
y: pos.y,
width: tex_size + border * 2.,
height: tex_size + border * 2.,
};
d.draw_rectangle_rec(
bounds,
if &option == current {
BG_WIDGET_ACTIVE
} else {
gray(16)
},
);
d.draw_texture_ex(
texture,
pos + Vector2::new(border, border),
0.,
tex_size / texture.width as f32,
Color::WHITE,
);
let mouse_pos = d.get_mouse_position();
if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT)
&& bounds.check_collision_point_rec(mouse_pos)
{
*current = option;
}
}
pub fn draw_usize(
d: &mut RaylibDrawHandle,
textures: &Textures,
number: usize,
x: i32,
y: i32,
digits: u8,
scale: u8,
) {
let digits = digits as i32;
let scale = scale as i32;
for i in 0..digits {
d.draw_rectangle(x + 10 * i * scale, y, 8 * scale, 16 * scale, BG_LIGHT);
}
let mut num = number;
let mut i = 0;
while (num != 0 || i == 0) && i < digits {
let texture = textures.get(&format!("digit_{}", num % 10));
let x = x + (digits - i - 1) * 10 * scale;
draw_scaled_texture(d, texture, x, y, scale as f32);
num /= 10;
i += 1;
}
}
pub fn draw_usize_small(
d: &mut RaylibDrawHandle,
textures: &Textures,
mut num: usize,
mut x: i32,
y: i32,
scale: f32,
) {
const MAX_DIGITS: usize = 8;
let mut digits = [0; MAX_DIGITS];
let mut i = 0;
while (num != 0 || i == 0) && i < MAX_DIGITS {
digits[MAX_DIGITS - i - 1] = num % 10;
num /= 10;
i += 1;
}
let texture = textures.get("digits_small");
for &digit in &digits[(MAX_DIGITS - i)..] {
let source = Rectangle::new(4. * digit as f32, 0., 4., 6.);
let dest = Rectangle::new(x as f32, y as f32, 4. * scale, 6. * scale);
d.draw_texture_pro(texture, source, dest, Vector2::zero(), 0., FG_MARBLE_VALUE);
x += 4 * scale as i32;
}
}
pub fn slider(
d: &mut RaylibDrawHandle,
value: &mut u8,
min: u8,
max: u8,
x: i32,
y: i32,
width: i32,
height: i32,
) -> bool {
// the +1 makes the lowest state look slightly filled and the max state fully filled
let percent = (*value - min + 1) as f32 / (max - min + 1) as f32;
d.draw_rectangle(x, y, width, height, BG_WIDGET);
d.draw_rectangle(x, y, (width as f32 * percent) as i32, height, Color::CYAN);
let mouse_pos = d.get_mouse_position();
let bounds = Rectangle::new(x as f32, y as f32, width as f32, height as f32);
if bounds.check_collision_point_rec(mouse_pos) {
if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) {
let percent = (mouse_pos.x - bounds.x) / bounds.width;
let new_value = min + (percent * (max - min + 1) as f32) as u8;
if *value != new_value {
*value = new_value;
}
} else if d.get_mouse_wheel_move() > 0.5 && *value < max {
*value += 1;
} else if d.get_mouse_wheel_move() < -0.5 && *value > min {
*value -= 1;
}
}
false
}
pub fn userdata_dir() -> PathBuf {
PathBuf::from("user")
}
@ -454,24 +43,6 @@ pub fn draw_scaled_texture(
d.draw_texture_ex(texture, pos, 0., scale, Color::WHITE);
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Scroll {
Up,
Down,
}
pub fn get_scroll(rl: &RaylibHandle) -> Option<Scroll> {
const SCROLL_THRESHOLD: f32 = 0.5;
let value = rl.get_mouse_wheel_move();
if value > SCROLL_THRESHOLD {
Some(Scroll::Up)
} else if value < -SCROLL_THRESHOLD {
Some(Scroll::Down)
} else {
None
}
}
pub fn get_free_id<T>(items: &[T], id_fn: fn(&T) -> usize) -> usize {
let mut id = 0;
while items.iter().any(|i| id_fn(i) == id) {