1use crate::window::{TitlePosition, Window};
2use anyhow::anyhow;
3use caw_persist::PersistData;
4use line_2d::Coord;
5use midly::num::u7;
6use sdl2::{
7 event::Event, keyboard::Scancode, mouse::MouseButton, pixels::Color,
8 rect::Rect,
9};
10use serde::{Deserialize, Serialize};
11use std::time::Instant;
12
13const WIDTH_PX: u32 = 128;
14const HEIGHT_PX: u32 = 128;
15
16const RANGE_RADS: f32 = (std::f32::consts::PI * 3.0) / 2.0;
17
18#[derive(Serialize, Deserialize, PartialEq, Clone, Copy, Debug)]
19struct State {
20 value_01: f32,
21}
22
23impl PersistData for State {
24 const NAME: &'static str = "knob_state";
25}
26
27pub struct Knob {
28 title: Option<String>,
29 window: Window,
30 state: State,
31 space_pressed: bool,
32 sensitivity: f32,
33}
34
35impl Knob {
36 pub fn new(
37 title: Option<&str>,
38 initial_value_01: f32,
39 sensitivity: f32,
40 ) -> anyhow::Result<Self> {
41 let window = Window::new(title, WIDTH_PX, HEIGHT_PX)?;
42 let state = if let Some(state) = title.and_then(|t| State::load_(t)) {
43 state
44 } else {
45 State {
46 value_01: initial_value_01.clamp(0., 1.),
47 }
48 };
49 Ok(Self {
50 title: title.map(|s| s.to_string()),
51 window,
52 state,
53 space_pressed: false,
54 sensitivity,
55 })
56 }
57
58 fn handle_events(&mut self) {
59 let prev_state = self.state;
60 for event in self.window.event_pump.poll_iter() {
61 Window::handle_event_common(
62 event.clone(),
63 self.window.title.as_ref(),
64 );
65 match event {
66 Event::KeyDown { scancode, .. } => {
67 let value_01 = match scancode {
68 Some(Scancode::Grave) => 0.0,
69 Some(Scancode::Num1) => 0.1,
70 Some(Scancode::Num2) => 0.2,
71 Some(Scancode::Num3) => 0.3,
72 Some(Scancode::Num4) => 0.4,
73 Some(Scancode::Num5) => 0.5,
74 Some(Scancode::Num6) => 0.6,
75 Some(Scancode::Num7) => 0.7,
76 Some(Scancode::Num8) => 0.8,
77 Some(Scancode::Num9) => 0.9,
78 Some(Scancode::Num0) => 1.0,
79 Some(Scancode::Space) => {
80 self.space_pressed = true;
81 continue;
82 }
83 _ => continue,
84 };
85 self.state.value_01 = value_01;
86 }
87 Event::KeyUp { scancode, .. } => match scancode {
88 Some(Scancode::Space) => self.space_pressed = false,
89 _ => (),
90 },
91 Event::MouseWheel { precise_y, .. } => {
92 let multiplier = 0.1;
93 self.state.value_01 = (self.state.value_01
94 + (precise_y * multiplier * self.sensitivity))
95 .clamp(0., 1.);
96 }
97 Event::MouseMotion {
98 mousestate, yrel, ..
99 } => {
100 if mousestate.is_mouse_button_pressed(MouseButton::Left) {
101 let multiplier = -0.05;
102 self.state.value_01 = (self.state.value_01
103 + (yrel as f32 * multiplier * self.sensitivity))
104 .clamp(0., 1.);
105 }
106 }
107 _ => (),
108 }
109 }
110 if let Some(title) = self.title.as_ref() {
111 if prev_state != self.state {
112 self.state.save_(title);
113 }
114 }
115 }
116
117 fn render_value(&mut self) -> anyhow::Result<()> {
118 let text_surface = self
119 .window
120 .font
121 .render(format!("{}", self.state.value_01).as_str())
122 .blended(Color::WHITE)
123 .map_err(|e| anyhow!("{e}"))?;
124 let text_texture =
125 text_surface.as_texture(&self.window.texture_creator)?;
126 let (canvas_width, canvas_height) = self
127 .window
128 .canvas
129 .output_size()
130 .map_err(|e| anyhow!("{e}"))?;
131 let text_texture_query = text_texture.query();
132 let text_rect = Rect::new(
134 (canvas_width as i32 - text_texture_query.width as i32) / 2,
135 canvas_height as i32 - text_texture_query.height as i32,
136 text_texture_query.width,
137 text_texture_query.height,
138 );
139 self.window
140 .canvas
141 .copy(&text_texture, None, Some(text_rect))
142 .map_err(|e| anyhow!("{e}"))?;
143 Ok(())
144 }
145
146 fn render_knob(&mut self) -> anyhow::Result<()> {
147 let (available_width, canvas_height) = self
148 .window
149 .canvas
150 .output_size()
151 .map_err(|e| anyhow!("{e}"))?;
152 let text_padding_px = 30;
153 let available_height = canvas_height - text_padding_px;
154 let centre = Coord {
155 x: available_width as i32 / 2,
156 y: available_height as i32 / 2,
157 };
158 let absolute_rotation_rads =
159 (((std::f32::consts::PI * 2.0) - RANGE_RADS) / 2.0)
160 + std::f32::consts::FRAC_PI_2;
161 let relative_angle_rads = RANGE_RADS * self.state.value_01;
162 let line_length_px =
163 ((available_width.min(available_height) / 2) - 10) as f32;
164 let angle_rads = absolute_rotation_rads + relative_angle_rads;
165 let end_dx = angle_rads.cos() * line_length_px;
166 let end_dy = angle_rads.sin() * line_length_px;
167 let end = centre
168 + Coord {
169 x: end_dx as i32,
170 y: end_dy as i32,
171 };
172 let line_width = 4;
173 self.window.canvas.set_draw_color(Color::WHITE);
174 for Coord { x, y } in line_2d::coords_between(centre, end) {
175 let rect = Rect::new(
176 x - (line_width as i32 / 2),
177 y - (line_width as i32 / 2),
178 line_width,
179 line_width,
180 );
181 self.window
182 .canvas
183 .fill_rect(rect)
184 .map_err(|e| anyhow!("{e}"))?;
185 }
186 Ok(())
187 }
188
189 fn render(&mut self) -> anyhow::Result<()> {
190 self.window.canvas.set_draw_color(Color::BLACK);
191 self.window.canvas.clear();
192 self.window.render_title(TitlePosition::CenterBottom)?;
193 self.render_value()?;
194 self.render_knob()?;
195 self.window.canvas.present();
196 Ok(())
197 }
198
199 fn update(&mut self) -> anyhow::Result<()> {
200 self.handle_events();
201 self.render()?;
202 Ok(())
203 }
204
205 pub fn tick(&mut self) -> anyhow::Result<()> {
207 self.window.wait_until_next_frame();
208 self.update()?;
209 self.window.prev_tick_complete = Instant::now();
210 Ok(())
211 }
212
213 pub fn value_01(&self) -> f32 {
214 self.state.value_01
215 }
216
217 pub fn value_midi(&self) -> u7 {
218 ((self.value_01() * u7::max_value().as_int() as f32) as u8).into()
219 }
220
221 pub fn is_space_pressed(&self) -> bool {
222 self.space_pressed
223 }
224}