move stuff around a bit

This commit is contained in:
Daniel Bulant 2026-05-12 18:14:17 +02:00
parent b14ac917d6
commit 8638b90cbe
No known key found for this signature in database
17 changed files with 1329 additions and 386 deletions

View file

@ -1,14 +1,15 @@
[build]
target = "xtensa-esp32-none-elf"
[target.xtensa-esp32-none-elf]
runner = "espflash flash --monitor"
rustflags = [
"-C", "link-arg=-Wl,-Tlinkall.x",
"-C", "link-arg=-nostartfiles",
"-C",
"link-arg=-Wl,-Tlinkall.x",
"-C",
"link-arg=-nostartfiles",
]
[build]
target = "xtensa-esp32-none-elf"
[unstable]
build-std = ["core", "alloc", "compiler_builtins"]

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
export
node_modules
target

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[workspace]
members = [
"device-state",
"esp32",
"pico",
]
resolver = "2"
[profile.release]
debug = true
debug-assertions = true
lto = "fat"
codegen-units = 1

10
device-state/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "device-state"
version = "0.1.0"
edition = "2024"
[dependencies]
owned_str = "0.1.2"
serde = { version = "1.0.228", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
ufmt = "0.2.0"

360
device-state/src/lib.rs Normal file
View file

@ -0,0 +1,360 @@
#![no_std]
extern crate alloc;
use alloc::string::String;
use core::str::FromStr;
use owned_str::OwnedStr;
use serde::{Deserialize, Serialize};
use ufmt::uwrite;
const ARROW_RIGHT: char = char::from_u32(0b0111_1110).unwrap();
const ARROW_LEFT: char = char::from_u32(0b0111_1111).unwrap();
const DOT: char = char::from_u32(0b1010_0101).unwrap();
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ViewState {
Loading,
Question,
Results,
}
#[derive(Deserialize, Clone, Copy)]
pub enum QuestionType {
Choice,
Numeric { min: i32, max: i32 },
}
#[derive(Clone)]
pub struct QuestionData {
pub text: OwnedStr<256>,
pub q_type: QuestionType,
pub points: i32,
pub index: usize,
}
#[derive(Deserialize)]
pub struct QuestionDataNet<'a> {
pub text: &'a str,
pub q_type: QuestionType,
pub points: i32,
pub index: usize,
}
#[derive(Deserialize)]
pub enum ProxyOutput<'a> {
Question(QuestionDataNet<'a>),
Results,
Error(&'a str),
}
impl<'a> From<QuestionDataNet<'a>> for QuestionData {
fn from(value: QuestionDataNet<'a>) -> Self {
QuestionData {
text: OwnedStr::from_str(value.text).unwrap(),
q_type: value.q_type,
points: value.points,
index: value.index,
}
}
}
#[derive(Clone, Copy)]
pub struct WheelData {
pub value: i32,
pub min: i32,
pub max: i32,
pub accumulated: i32,
}
impl WheelData {
pub const fn empty() -> Self {
Self {
value: 0,
min: 0,
max: 0,
accumulated: 0,
}
}
}
pub struct DeviceState {
view: ViewState,
question: Option<QuestionData>,
wheel: WheelData,
last_index: usize,
title_offset: usize,
}
impl DeviceState {
pub const fn new() -> Self {
Self {
view: ViewState::Loading,
question: None,
wheel: WheelData::empty(),
last_index: 0,
title_offset: 0,
}
}
pub fn reset(&mut self) {
self.question = None;
self.wheel = WheelData::empty();
self.last_index = 0;
self.title_offset = 0;
}
pub fn view_state(&self) -> ViewState {
self.view
}
pub fn wheel(&self) -> WheelData {
self.wheel
}
pub fn wheel_mut(&mut self) -> &mut WheelData {
&mut self.wheel
}
pub fn apply_proxy_output(&mut self, data: ProxyOutput<'_>) {
match data {
ProxyOutput::Question(data) => {
let data: QuestionData = data.into();
let mut future_wheel = WheelData::empty();
if let QuestionType::Numeric { min, max } = data.q_type {
future_wheel.max = max;
future_wheel.min = min;
future_wheel.value = (min + max) / 2;
}
self.question = Some(data);
self.view = ViewState::Question;
self.wheel = future_wheel;
}
ProxyOutput::Results => {
self.view = ViewState::Results;
}
ProxyOutput::Error(_) => {}
}
}
pub fn question(&self) -> Option<&QuestionData> {
self.question.as_ref()
}
pub fn tick(&mut self) {
if self.view != ViewState::Question {
return;
}
let Some(question) = self.question.as_ref() else {
return;
};
self.title_offset = self.title_offset.wrapping_add(1);
if question.index != self.last_index {
self.last_index = question.index;
self.title_offset = 0;
}
}
pub fn render_lines(&mut self) -> Option<(OwnedStr<16>, OwnedStr<16>)> {
if self.view != ViewState::Question {
return None;
}
let question = self.question.as_ref()?;
let title_line = if question.text.len() > 16 {
self.title_offset %= question.text.len() - 16;
OwnedStr::from_str(&question.text[self.title_offset..self.title_offset + 16]).unwrap()
} else {
OwnedStr::from_str(&question.text).unwrap()
};
let number_str: OwnedStr<16> = match question.q_type {
QuestionType::Choice => {
let mut writer = OwnedStrWriter::new();
writer.push(DOT).unwrap();
writer.push(' ').unwrap();
uwrite!(writer, "{}", question.points).unwrap();
writer.into()
}
QuestionType::Numeric { min, max } => {
let mut writer = OwnedStrWriter::new();
if self.wheel.value > min {
writer.push(ARROW_LEFT).unwrap();
writer.push(' ').unwrap();
}
uwrite!(writer, "{}", self.wheel.value).unwrap();
if self.wheel.value < max {
writer.push(ARROW_RIGHT).unwrap();
writer.push(' ').unwrap();
}
writer.into()
}
};
let second_line = center_str::<16>(&number_str, 16).unwrap();
Some((title_line, second_line))
}
pub fn response_value(&self, button: u8) -> i32 {
match self.question.as_ref() {
Some(q) => match q.q_type {
QuestionType::Numeric { .. } => self.wheel.value,
QuestionType::Choice => button as i32,
},
_ => button as i32,
}
}
}
#[derive(Serialize)]
pub enum WriteType<'a> {
QuizResponse(i32),
DeviceId(&'a str),
}
pub fn parse_proxy_output<'a>(input: &'a str) -> Result<ProxyOutput<'a>, serde_json::Error> {
serde_json::from_str::<ProxyOutput<'a>>(input)
}
pub fn serialize_write(data: &WriteType<'_>) -> Result<String, serde_json::Error> {
serde_json::to_string(data)
}
pub const WHEEL_TICKS: i32 = 4096;
pub fn wheel_delta(old_angle: i32, current_angle: i32, inverted: bool) -> i32 {
let mut diff = current_angle - old_angle;
if diff.abs() > WHEEL_TICKS / 2 {
diff = if diff > 0 {
diff - WHEEL_TICKS
} else {
diff + WHEEL_TICKS
};
}
if inverted {
-diff
} else {
diff
}
}
pub fn apply_wheel_delta(
value: &mut i32,
min: i32,
max: i32,
accumulated: &mut i32,
diff: i32,
precision: i32,
) {
if max == min || precision <= 0 {
return;
}
*accumulated += diff;
let step_count = *accumulated / precision;
if step_count == 0 {
return;
}
*accumulated -= step_count * precision;
*value += step_count;
if *value < min {
*value = min;
} else if *value > max {
*value = max;
}
if *value == min || *value == max {
*accumulated = 0;
}
}
pub struct OwnedStrWriter<const CAP: usize>(OwnedStr<CAP>);
impl<const CAP: usize> OwnedStrWriter<CAP> {
pub fn new() -> Self {
Self(OwnedStr::new())
}
pub fn push(&mut self, c: char) -> Result<(), owned_str::Error> {
self.0.try_push(c).map(|_| ())
}
}
impl<const CAP: usize> From<OwnedStr<CAP>> for OwnedStrWriter<CAP> {
fn from(owned_str: OwnedStr<CAP>) -> Self {
Self(owned_str)
}
}
impl<const CAP: usize> From<OwnedStrWriter<CAP>> for OwnedStr<CAP> {
fn from(writer: OwnedStrWriter<CAP>) -> Self {
writer.0
}
}
impl<const CAP: usize> ufmt::uWrite for OwnedStrWriter<CAP> {
type Error = owned_str::Error;
fn write_str(&mut self, s: &str) -> Result<(), Self::Error> {
self.0.try_push_str(s).map(|_| ())
}
fn write_char(&mut self, c: char) -> Result<(), Self::Error> {
self.0.try_push(c).map(|_| ())
}
}
pub fn center_str<const CAP: usize>(text: &str, width: usize) -> Result<OwnedStr<CAP>, owned_str::Error> {
let mut res = OwnedStr::new();
let len = text.len();
let padding = (width.saturating_sub(len) + 1) / 2;
for _ in 0..padding {
res.try_push(' ')?;
}
res.try_push_str(text)?;
Ok(res)
}
#[cfg(test)]
mod tests {
use super::{apply_wheel_delta, wheel_delta};
#[test]
fn wraps_forward_across_zero() {
assert_eq!(wheel_delta(4090, 5, false), 11);
}
#[test]
fn wraps_backward_across_zero() {
assert_eq!(wheel_delta(5, 4090, false), -11);
}
#[test]
fn inverts_direction() {
assert_eq!(wheel_delta(10, 20, true), -10);
}
#[test]
fn accumulates_before_applying_selection() {
let mut value = 5;
let mut accumulated = 0;
apply_wheel_delta(&mut value, 0, 10, &mut accumulated, 10, 32);
assert_eq!(value, 5);
assert_eq!(accumulated, 10);
apply_wheel_delta(&mut value, 0, 10, &mut accumulated, 22, 32);
assert_eq!(value, 6);
assert_eq!(accumulated, 0);
}
#[test]
fn clamps_and_resets_at_bounds() {
let mut value = 10;
let mut accumulated = 0;
apply_wheel_delta(&mut value, 0, 10, &mut accumulated, 64, 32);
assert_eq!(value, 10);
assert_eq!(accumulated, 0);
}
}

View file

@ -3,17 +3,11 @@ name = "esp32"
version = "0.1.0"
edition = "2024"
[profile.release]
debug = true
debug-assertions = true
lto = "fat"
codegen-units = 1
[dependencies]
[dependencies.device-state]
path = "../device-state"
[target.'cfg(target_arch = "xtensa")'.dependencies]
serde = { version = "1.0.228", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
arrayvec = { version = "0.7.6", default-features = false }
ag-lcd = { version = "0.3", features = ["ufmt"] }
as5600 = "0.8.0"
@ -36,19 +30,15 @@ embassy-time = { version = "0.5.1", features = [
esp-backtrace = { version = "0.19.0", features = ["esp32", "println"] }
static_cell = "2.1.1"
log = "0.4"
ufmt = "0.2.0"
owned_str = "0.1.2"
[target.'cfg(target_arch = "arm")'.dependencies]
embedded-hal-compat = "0.13.0"
ufmt = "0.2.0"
log = "0.4"
ag-lcd={ version = "0.3", features = ["ufmt"]}
as5600 = "0.8.0"
embassy-futures = "0.1.2"
embedded-io-async = "0.6.1"
embedded-io = "0.7.1"
owned_str = "0.1.2"
embassy-sync = "0.8.0"
embassy-net = { version = "0.9.1",features = ["defmt", "icmp", "tcp", "udp", "raw", "dhcpv4", "medium-ethernet", "dns", "proto-ipv4", "proto-ipv6", "multicast"]}
embassy-executor = { version = "0.10.0", features = [

View file

@ -7,8 +7,8 @@ use esp_hal::peripherals::{GPIO21, GPIO22, I2C0};
use esp_hal::time::Rate;
use esp_println::println;
use crate::WHEEL_VALUE;
use esp32::{apply_wheel_delta, wheel_delta};
use crate::STATE;
use device_state::{apply_wheel_delta, wheel_delta};
pub static INPUT: Channel<CriticalSectionRawMutex, u8, 64> = Channel::new();
pub static ANGLE: Mutex<CriticalSectionRawMutex, u16> = Mutex::new(0);
@ -39,7 +39,8 @@ pub async fn rotation_read_task(config: RotationConfig) {
*locked = angle;
drop(locked);
let diff = wheel_delta(old, angle as i32, crate::WHEEL_INVERTED);
let mut wheel = WHEEL_VALUE.lock().await;
let mut state = STATE.lock().await;
let wheel = state.wheel();
if wheel.max != wheel.min {
let min = wheel.min;
let max = wheel.max;
@ -47,6 +48,7 @@ pub async fn rotation_read_task(config: RotationConfig) {
let mut accumulated = wheel.accumulated;
let precision = crate::WHEEL_PRECISION;
apply_wheel_delta(&mut value, min, max, &mut accumulated, diff, precision);
let wheel = state.wheel_mut();
wheel.value = value;
wheel.accumulated = accumulated;
}

View file

@ -1,5 +1 @@
#![no_std]
mod wheel;
pub use wheel::{apply_wheel_delta, wheel_delta};

View file

@ -3,16 +3,13 @@
extern crate alloc;
use core::str::FromStr;
use embassy_executor::Spawner;
use embassy_futures::select::{Either, select};
use embassy_net::tcp::{State, TcpReader, TcpWriter};
use embassy_net::tcp::{TcpReader, TcpWriter};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_sync::signal::Signal;
use embassy_time::Timer;
use embedded_io::Write;
use esp_backtrace as _;
use esp_hal::{
gpio::{Input, InputConfig as GpioInputConfig, Pin, Pull},
@ -20,14 +17,11 @@ use esp_hal::{
timer::timg::TimerGroup,
};
use esp_println::println;
use owned_str::OwnedStr;
use serde::{Deserialize, Serialize};
use ufmt::uwrite;
use device_state::{DeviceState, ViewState};
mod buffer;
mod input;
mod net;
mod owned_str_writer;
mod screen;
pub use input::ANGLE;
@ -42,12 +36,6 @@ const WHEEL_PRECISION: i32 = 32;
const WHEEL_INVERTED: bool = false;
const DEVICE_ID: &str = "esp32-1";
#[derive(Deserialize)]
enum QuestionType {
Choice,
Numeric { min: i32, max: i32 },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TcpDisconnect {
ReadError,
@ -56,72 +44,11 @@ pub enum TcpDisconnect {
Cancelled,
}
struct QuestionData {
text: OwnedStr<256>,
q_type: QuestionType,
points: i32,
index: usize,
}
#[derive(Deserialize)]
struct QuestionDataNet<'a> {
text: &'a str,
q_type: QuestionType,
points: i32,
index: usize,
}
#[derive(Deserialize)]
enum ProxyOutput<'a> {
Question(QuestionDataNet<'a>),
Results,
Error(&'a str),
}
impl<'a> From<QuestionDataNet<'a>> for QuestionData {
fn from(value: QuestionDataNet<'a>) -> Self {
QuestionData {
text: OwnedStr::from_str(value.text).unwrap(),
q_type: value.q_type,
points: value.points,
index: value.index,
}
}
}
#[derive(Clone, Copy)]
struct WheelData {
value: i32,
min: i32,
max: i32,
accumulated: i32,
}
#[derive(Clone, Copy)]
enum MainState {
Loading,
Question,
Results,
}
static MAIN_STATE: Mutex<CriticalSectionRawMutex, MainState> = Mutex::new(MainState::Loading);
static QUESTION: Mutex<CriticalSectionRawMutex, Option<QuestionData>> = Mutex::new(None);
static QUESTION_UPDATE: Signal<CriticalSectionRawMutex, ()> = Signal::new();
static WHEEL_VALUE: Mutex<CriticalSectionRawMutex, WheelData> = Mutex::new(WheelData {
value: 0,
min: 0,
max: 0,
accumulated: 0,
});
pub static STATE: Mutex<CriticalSectionRawMutex, DeviceState> = Mutex::new(DeviceState::new());
pub async fn reset_state() {
*QUESTION.lock().await = None;
*WHEEL_VALUE.lock().await = WheelData {
value: 0,
min: 0,
max: 0,
accumulated: 0,
};
let mut state = STATE.lock().await;
state.reset();
}
#[panic_handler]
@ -155,59 +82,23 @@ pub async fn tcp_read_loop(
let Ok(str) = core::str::from_utf8(&buf[..len]) else {
continue;
};
let mut question_data = None;
let mut future_wheel = WheelData {
value: 0,
min: 0,
max: 0,
accumulated: 0,
};
if let Some(last) = str.lines().last() {
let Ok(data) = serde_json::from_str::<ProxyOutput>(last) else {
let Ok(data) = device_state::parse_proxy_output(last) else {
continue;
};
match data {
ProxyOutput::Question(data) => {
let data: QuestionData = data.into();
match data.q_type {
QuestionType::Numeric { min, max } => {
future_wheel.max = max;
future_wheel.min = min;
future_wheel.value = (min + max) / 2;
}
_ => {}
};
question_data = Some(data);
}
ProxyOutput::Results => {
*MAIN_STATE.lock().await = MainState::Results;
}
ProxyOutput::Error(e) => {}
}
}
if let Some(question_data) = question_data {
*QUESTION.lock().await = Some(question_data);
*MAIN_STATE.lock().await = MainState::Question;
*WHEEL_VALUE.lock().await = future_wheel;
QUESTION_UPDATE.signal(());
let mut state = STATE.lock().await;
state.apply_proxy_output(data);
}
}
}
#[derive(Serialize)]
enum WriteType<'a> {
QuizResponse(i32),
DeviceId(&'a str),
}
pub async fn tcp_write_loop(
mut write: TcpWriter<'_>,
cancel: &Signal<CriticalSectionRawMutex, ()>,
) -> Result<(), TcpDisconnect> {
if write
.write(
serde_json::to_string(&WriteType::DeviceId(DEVICE_ID))
device_state::serialize_write(&device_state::WriteType::DeviceId(DEVICE_ID))
.unwrap()
.as_bytes(),
)
@ -225,19 +116,9 @@ pub async fn tcp_write_loop(
Either::Second(()) => return Err(TcpDisconnect::Cancelled),
};
println!("button={}", data);
let value = {
let question = QUESTION.lock().await;
let wheel = *WHEEL_VALUE.lock().await;
match question.as_ref() {
Some(q) => match q.q_type {
QuestionType::Numeric { .. } => wheel.value,
QuestionType::Choice => data as _,
},
_ => data as _,
}
};
let data = WriteType::QuizResponse(value);
let buffer = serde_json::to_string(&data).unwrap();
let value = STATE.lock().await.response_value(data);
let data = device_state::WriteType::QuizResponse(value);
let buffer = device_state::serialize_write(&data).unwrap();
println!("write: {}", &buffer);
if write.write(buffer.as_bytes()).await.is_err() {
cancel.signal(());
@ -246,72 +127,31 @@ pub async fn tcp_write_loop(
}
}
const ARROW_RIGHT: char = char::from_u32(0b0111_1110).unwrap();
const ARROW_LEFT: char = char::from_u32(0b0111_1111).unwrap();
const DOT: char = char::from_u32(0b1010_0101).unwrap();
#[embassy_executor::task]
pub async fn main_loop() {
let mut last_index = 0;
let mut title_offset = 0;
println!("Main loop started");
loop {
embassy_time::Timer::after_millis(50).await;
let state = *MAIN_STATE.lock().await;
let mut state = STATE.lock().await;
state.tick();
let lines = state.render_lines();
let view = state.view_state();
drop(state);
match state {
MainState::Loading => {
continue;
}
MainState::Question => {}
MainState::Results => {
match view {
ViewState::Loading => continue,
ViewState::Results => {
overwrite_lcd("Results", "").await;
continue;
}
ViewState::Question => {}
}
let wheel = *WHEEL_VALUE.lock().await;
let question = QUESTION.lock().await;
let Some(question) = question.as_ref() else {
let Some((title_line, second_line)) = lines else {
continue;
};
title_offset += 1;
if question.index != last_index {
last_index = question.index;
title_offset = 0;
}
let title_line = if question.text.len() > 16 {
title_offset %= question.text.len() - 16;
&question.text[title_offset..title_offset + 16]
} else {
&question.text
};
let number_str: OwnedStr<16> = match question.q_type {
QuestionType::Choice => {
let mut writer = owned_str_writer::OwnedStrWriter::new();
writer.push(DOT).unwrap();
writer.push(' ').unwrap();
uwrite!(writer, "{}", question.points).unwrap();
writer.into()
}
QuestionType::Numeric { min, max } => {
let mut writer = owned_str_writer::OwnedStrWriter::new();
if wheel.value > min {
writer.push(ARROW_LEFT).unwrap();
writer.push(' ').unwrap();
}
uwrite!(writer, "{}", wheel.value).unwrap();
if wheel.value < max {
writer.push(ARROW_RIGHT).unwrap();
writer.push(' ').unwrap();
}
writer.into()
}
};
let second_line = owned_str_writer::center_str::<16>(&number_str, 16).unwrap();
println!("lcd: {} {}", title_line, second_line);
screen::overwrite_lcd(title_line, &second_line).await;
screen::overwrite_lcd(&title_line, &second_line).await;
}
}

View file

@ -14,7 +14,7 @@ use esp_radio::wifi::sta::StationConfig;
use esp_radio::wifi::{Config, ControllerConfig, scan::ScanConfig};
use crate::screen::overwrite_lcd;
use crate::{TcpDisconnect, buffer::wait_for_config, tcp_read_loop, tcp_write_loop};
use crate::{buffer::wait_for_config, tcp_read_loop, tcp_write_loop};
use crate::{WIFI_NETWORK, WIFI_PASSWORD};
pub struct NetworkConfig<'a> {

View file

@ -1,50 +0,0 @@
use owned_str::{Error, OwnedStr};
use ufmt::uWrite;
pub struct OwnedStrWriter<const CAP: usize>(OwnedStr<CAP>);
impl<const CAP: usize> OwnedStrWriter<CAP> {
pub fn new() -> Self {
Self(OwnedStr::new())
}
pub fn push(&mut self, c: char) -> Result<(), Error> {
self.0.try_push(c).map(|_| ())
}
}
impl<const CAP: usize> From<OwnedStr<CAP>> for OwnedStrWriter<CAP> {
fn from(owned_str: OwnedStr<CAP>) -> Self {
Self(owned_str)
}
}
impl<const CAP: usize> From<OwnedStrWriter<CAP>> for OwnedStr<CAP> {
fn from(writer: OwnedStrWriter<CAP>) -> Self {
writer.0
}
}
impl<const CAP: usize> uWrite for OwnedStrWriter<CAP> {
type Error = owned_str::Error;
fn write_str(&mut self, s: &str) -> Result<(), Self::Error> {
self.0.try_push_str(s).map(|_| ())
}
fn write_char(&mut self, c: char) -> Result<(), Self::Error> {
self.0.try_push(c).map(|_| ())
}
}
pub fn center_str<const CAP: usize>(
str: &str,
width: usize,
) -> Result<OwnedStr<CAP>, owned_str::Error> {
let mut res = OwnedStr::new();
let len = str.len();
let padding = (width.saturating_sub(len) + 1) / 2;
for _ in 0..padding {
res.try_push(' ')?;
}
res.try_push_str(str)?;
Ok(res)
}

View file

@ -1,48 +0,0 @@
pub const WHEEL_TICKS: i32 = 4096;
pub fn wheel_delta(old_angle: i32, current_angle: i32, inverted: bool) -> i32 {
let mut diff = current_angle - old_angle;
if diff.abs() > WHEEL_TICKS / 2 {
diff = if diff > 0 {
diff - WHEEL_TICKS
} else {
diff + WHEEL_TICKS
};
}
if inverted {
-diff
} else {
diff
}
}
pub fn apply_wheel_delta(
value: &mut i32,
min: i32,
max: i32,
accumulated: &mut i32,
diff: i32,
precision: i32,
) {
if max == min || precision <= 0 {
return;
}
*accumulated += diff;
let step_count = *accumulated / precision;
if step_count == 0 {
return;
}
*accumulated -= step_count * precision;
*value += step_count;
if *value < min {
*value = min;
} else if *value > max {
*value = max;
}
if *value == min || *value == max {
*accumulated = 0;
}
}

View file

@ -1 +0,0 @@
rustc --test wheel-tests.rs -o wheel-tests && ./wheel-tests

View file

@ -1,46 +0,0 @@
#![allow(dead_code)]
mod wheel {
include!("src/wheel.rs");
}
use wheel::{apply_wheel_delta, wheel_delta};
#[test]
fn wraps_forward_across_zero() {
assert_eq!(wheel_delta(4090, 5, false), 11);
}
#[test]
fn wraps_backward_across_zero() {
assert_eq!(wheel_delta(5, 4090, false), -11);
}
#[test]
fn inverts_direction() {
assert_eq!(wheel_delta(10, 20, true), -10);
}
#[test]
fn accumulates_before_applying_selection() {
let mut value = 5;
let mut accumulated = 0;
apply_wheel_delta(&mut value, 0, 10, &mut accumulated, 10, 32);
assert_eq!(value, 5);
assert_eq!(accumulated, 10);
apply_wheel_delta(&mut value, 0, 10, &mut accumulated, 22, 32);
assert_eq!(value, 6);
assert_eq!(accumulated, 0);
}
#[test]
fn clamps_and_resets_at_bounds() {
let mut value = 10;
let mut accumulated = 0;
apply_wheel_delta(&mut value, 0, 10, &mut accumulated, 64, 32);
assert_eq!(value, 10);
assert_eq!(accumulated, 0);
}