caw_widgets/
knob.rs

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        // Render the title centred at the bottom of the window.
133        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    /// Waits until the next frame, then handles events and redraws the widget
206    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}