1use std::time::Instant;
6
7use dais_core::bus::CommandSender;
8use dais_core::commands::Command;
9use dais_core::keybindings::{Action, KeyCombo, KeybindingMap};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum InputMode {
14 Normal,
16 Overview,
18 Ink,
20 Laser,
22 NotesEdit,
24 JumpToSlide,
26 TextBox,
28}
29
30#[derive(Debug, Clone, Copy, Default)]
32pub struct ActiveAids {
33 pub ink: bool,
34 pub laser: bool,
35 pub spotlight: bool,
36 pub zoom: bool,
37}
38
39#[derive(Debug, Clone, Copy, Default)]
40pub struct UiModes {
41 pub overview_visible: bool,
42 pub ink_active: bool,
43 pub laser_active: bool,
44 pub notes_editing: bool,
45 pub text_box_mode: bool,
46 pub text_box_editing: bool,
47 pub selected_text_box: Option<u64>,
48}
49
50pub struct InputHandler {
52 sender: CommandSender,
53 keybindings: KeybindingMap,
54 mode: InputMode,
55 jump_buffer: String,
56 jump_start: Option<Instant>,
57 stroke_in_progress: bool,
60}
61
62const JUMP_TIMEOUT_SECS: f64 = 3.0;
64const DEFAULT_ZOOM_FACTOR: f32 = 1.5;
66const ZOOM_STEPS: &[f32] = &[1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0];
68
69impl InputHandler {
70 pub fn new(sender: CommandSender, keybindings: KeybindingMap) -> Self {
71 Self {
72 sender,
73 keybindings,
74 mode: InputMode::Normal,
75 jump_buffer: String::new(),
76 jump_start: None,
77 stroke_in_progress: false,
78 }
79 }
80
81 pub fn handle_input(&mut self, ctx: &egui::Context, modes: UiModes) {
85 if self.mode != InputMode::JumpToSlide {
87 if modes.overview_visible {
88 self.mode = InputMode::Overview;
89 } else if modes.notes_editing {
90 self.mode = InputMode::NotesEdit;
91 } else if modes.ink_active {
92 self.mode = InputMode::Ink;
93 } else if modes.laser_active {
94 self.mode = InputMode::Laser;
95 } else if modes.text_box_mode {
96 self.mode = InputMode::TextBox;
97 } else {
98 self.mode = InputMode::Normal;
99 }
100 }
101
102 if self.mode == InputMode::JumpToSlide
104 && let Some(start) = self.jump_start
105 && start.elapsed().as_secs_f64() > JUMP_TIMEOUT_SECS
106 {
107 self.cancel_jump();
108 }
109
110 if self.mode == InputMode::NotesEdit {
111 self.process_notes_editor_keys(ctx);
112 return;
113 }
114
115 if self.mode == InputMode::TextBox && modes.text_box_editing {
116 self.process_text_box_editor_keys(ctx, modes.selected_text_box);
118 return;
119 }
120
121 if self.mode == InputMode::TextBox {
122 self.process_text_box_mode_keys(ctx, modes.selected_text_box);
123 return;
124 }
125
126 self.process_keys(ctx);
127 }
128
129 fn process_notes_editor_keys(&mut self, ctx: &egui::Context) {
130 let events: Vec<egui::Event> = ctx.input(|i| i.events.clone());
131
132 for event in &events {
133 if let egui::Event::Key { key, pressed: true, modifiers, .. } = event {
134 if *key == egui::Key::Escape {
135 let _ = self.sender.send(Command::ToggleNotesEdit);
136 continue;
137 }
138
139 let combo = egui_to_key_combo(*key, *modifiers);
140 if let Some(action) = self.keybindings.lookup(&combo) {
141 match action {
142 Action::SaveSidecar | Action::ToggleNotesEdit => {
143 self.dispatch_action(action);
144 }
145 _ => {}
146 }
147 }
148 }
149 }
150 }
151
152 fn process_text_box_editor_keys(&mut self, ctx: &egui::Context, _selected: Option<u64>) {
153 let events: Vec<egui::Event> = ctx.input(|i| i.events.clone());
155 for event in &events {
156 if let egui::Event::Key { key: egui::Key::Escape, pressed: true, .. } = event {
157 let _ = self.sender.send(Command::DeselectTextBox);
158 }
159 }
160 }
161
162 fn process_text_box_mode_keys(&mut self, ctx: &egui::Context, selected: Option<u64>) {
163 let events: Vec<egui::Event> = ctx.input(|i| i.events.clone());
164 for event in &events {
165 if let egui::Event::Key { key, pressed: true, modifiers, .. } = event {
166 match key {
167 egui::Key::Escape => {
168 if selected.is_some() {
169 let _ = self.sender.send(Command::DeselectTextBox);
170 } else {
171 let _ = self.sender.send(Command::ToggleTextBoxMode);
172 }
173 }
174 egui::Key::Delete | egui::Key::Backspace => {
175 if let Some(id) = selected {
176 let _ = self.sender.send(Command::DeleteTextBox { id });
177 }
178 }
179 egui::Key::Enter => {
180 if let Some(id) = selected {
181 let _ = self.sender.send(Command::BeginTextBoxEdit { id });
182 }
183 }
184 _ => {
185 let combo = egui_to_key_combo(*key, *modifiers);
186 if let Some(action) = self.keybindings.lookup(&combo) {
187 match action {
189 Action::SaveSidecar | Action::ToggleTextBoxMode | Action::Quit => {
190 self.dispatch_action(action);
191 }
192 _ => {}
193 }
194 }
195 }
196 }
197 }
198 }
199 }
200
201 fn process_keys(&mut self, ctx: &egui::Context) {
202 let events: Vec<egui::Event> = ctx.input(|i| i.events.clone());
204
205 for event in &events {
206 if let egui::Event::Key { key, pressed: true, modifiers, .. } = event {
207 self.handle_key(*key, *modifiers);
208 }
209 }
210 }
211
212 fn handle_key(&mut self, key: egui::Key, modifiers: egui::Modifiers) {
213 if self.mode == InputMode::JumpToSlide {
215 if let Some(digit) = key_to_digit(key) {
216 self.jump_buffer.push(digit);
217 return;
218 }
219 match key {
220 egui::Key::Enter => {
221 if let Ok(page_num) = self.jump_buffer.parse::<usize>() {
222 let index = page_num.saturating_sub(1);
223 let _ = self.sender.send(Command::GoToSlide(index));
224 }
225 self.cancel_jump();
226 return;
227 }
228 egui::Key::Escape => {
229 self.cancel_jump();
230 return;
231 }
232 _ => {
233 self.cancel_jump();
234 }
235 }
236 }
237
238 let combo = egui_to_key_combo(key, modifiers);
239 if let Some(action) = self.keybindings.lookup(&combo) {
240 self.dispatch_action(action);
241 }
242 }
243
244 fn dispatch_action(&mut self, action: Action) {
245 match action {
246 Action::GoToSlide => {
247 self.mode = InputMode::JumpToSlide;
248 self.jump_buffer.clear();
249 self.jump_start = Some(Instant::now());
250 }
251 Action::StartPauseTimer => {
252 let _ = self.sender.send(Command::ToggleTimer);
253 }
254 _ => {
255 if let Some(cmd) = action_to_command(action) {
256 let _ = self.sender.send(cmd);
257 }
258 }
259 }
260 }
261
262 fn cancel_jump(&mut self) {
263 self.mode = InputMode::Normal;
264 self.jump_buffer.clear();
265 self.jump_start = None;
266 }
267
268 pub fn handle_slide_mouse(
273 &mut self,
274 response: &egui::Response,
275 image_rect: egui::Rect,
276 aids: ActiveAids,
277 current_zoom_factor: Option<f32>,
278 ) {
279 if let Some(pos) = response.hover_pos() {
280 let norm = normalize_to_rect(pos, image_rect);
281 if (0.0..=1.0).contains(&norm.0) && (0.0..=1.0).contains(&norm.1) {
282 if aids.laser || aids.spotlight {
283 let _ = self.sender.send(Command::SetPointerPosition(norm.0, norm.1));
284 if aids.spotlight {
285 let _ = self.sender.send(Command::SetSpotlightPosition(norm.0, norm.1));
286 }
287 }
288
289 if aids.zoom {
290 let scroll_delta = response.ctx.input(|i| i.raw_scroll_delta.y);
291 let current_factor = current_zoom_factor.unwrap_or(DEFAULT_ZOOM_FACTOR);
292 let factor = if response.hovered() && scroll_delta.abs() > f32::EPSILON {
293 step_zoom_factor(current_factor, scroll_delta)
294 } else {
295 current_factor
296 };
297 let _ = self
298 .sender
299 .send(Command::SetZoomRegion { center: (norm.0, norm.1), factor });
300 }
301 }
302 }
303
304 let pointer_down = response.ctx.input(|i| i.pointer.primary_down());
305 if aids.ink
306 && pointer_down
307 && response.contains_pointer()
308 && let Some(pos) = response
309 .interact_pointer_pos()
310 .or_else(|| response.ctx.input(|i| i.pointer.latest_pos()))
311 {
312 let norm = normalize_to_rect(pos, image_rect);
313 if (0.0..=1.0).contains(&norm.0) && (0.0..=1.0).contains(&norm.1) {
314 let _ = self.sender.send(Command::AddInkPoint(norm.0, norm.1));
315 self.stroke_in_progress = true;
316 }
317 }
318
319 if aids.ink && self.stroke_in_progress && !pointer_down {
325 let _ = self.sender.send(Command::FinishInkStroke);
326 self.stroke_in_progress = false;
327 }
328 }
329
330 pub fn mode(&self) -> InputMode {
331 self.mode
332 }
333
334 pub fn jump_buffer(&self) -> &str {
335 &self.jump_buffer
336 }
337
338 pub fn keybindings(&self) -> &KeybindingMap {
340 &self.keybindings
341 }
342}
343
344fn step_zoom_factor(current_factor: f32, scroll_delta: f32) -> f32 {
345 let current_index = ZOOM_STEPS
346 .iter()
347 .position(|step| (*step - current_factor).abs() < f32::EPSILON)
348 .unwrap_or_else(|| nearest_zoom_step_index(current_factor));
349
350 let next_index = if scroll_delta > 0.0 {
351 current_index.saturating_add(1).min(ZOOM_STEPS.len() - 1)
352 } else {
353 current_index.saturating_sub(1)
354 };
355
356 ZOOM_STEPS[next_index]
357}
358
359fn nearest_zoom_step_index(current_factor: f32) -> usize {
360 ZOOM_STEPS
361 .iter()
362 .enumerate()
363 .min_by(|(_, left), (_, right)| {
364 (current_factor - **left)
365 .abs()
366 .partial_cmp(&(current_factor - **right).abs())
367 .unwrap_or(std::cmp::Ordering::Equal)
368 })
369 .map_or(0, |(index, _)| index)
370}
371
372pub fn normalize_to_rect(pos: egui::Pos2, rect: egui::Rect) -> (f32, f32) {
374 let x = (pos.x - rect.min.x) / rect.width();
375 let y = (pos.y - rect.min.y) / rect.height();
376 (x, y)
377}
378
379fn egui_to_key_combo(key: egui::Key, modifiers: egui::Modifiers) -> KeyCombo {
381 let key_name = egui_key_name(key);
382 KeyCombo {
383 key: key_name,
384 shift: modifiers.shift,
385 ctrl: modifiers.ctrl || modifiers.command,
386 alt: modifiers.alt,
387 }
388}
389
390fn egui_key_name(key: egui::Key) -> String {
392 egui_key_name_public(key)
393}
394
395pub fn egui_key_name_public(key: egui::Key) -> String {
397 match key {
398 egui::Key::ArrowRight => "Right".into(),
399 egui::Key::ArrowLeft => "Left".into(),
400 egui::Key::ArrowUp => "Up".into(),
401 egui::Key::ArrowDown => "Down".into(),
402 egui::Key::Space => "Space".into(),
403 egui::Key::Enter => "Enter".into(),
404 egui::Key::Escape => "Escape".into(),
405 egui::Key::Home => "Home".into(),
406 egui::Key::End => "End".into(),
407 egui::Key::PageUp => "PageUp".into(),
408 egui::Key::PageDown => "PageDown".into(),
409 egui::Key::Tab => "Tab".into(),
410 egui::Key::Backspace => "Backspace".into(),
411 egui::Key::Delete => "Delete".into(),
412 egui::Key::F1 => "F1".into(),
413 egui::Key::F2 => "F2".into(),
414 egui::Key::F3 => "F3".into(),
415 egui::Key::F4 => "F4".into(),
416 egui::Key::F5 => "F5".into(),
417 egui::Key::F6 => "F6".into(),
418 egui::Key::F7 => "F7".into(),
419 egui::Key::F8 => "F8".into(),
420 egui::Key::F9 => "F9".into(),
421 egui::Key::F10 => "F10".into(),
422 egui::Key::F11 => "F11".into(),
423 egui::Key::F12 => "F12".into(),
424 egui::Key::Minus => "-".into(),
425 egui::Key::Plus => "+".into(),
426 egui::Key::Equals => "=".into(),
427 egui::Key::Period => ".".into(),
428 other => {
429 let debug = format!("{other:?}");
431 debug.to_lowercase()
432 }
433 }
434}
435
436fn key_to_digit(key: egui::Key) -> Option<char> {
438 match key {
439 egui::Key::Num0 => Some('0'),
440 egui::Key::Num1 => Some('1'),
441 egui::Key::Num2 => Some('2'),
442 egui::Key::Num3 => Some('3'),
443 egui::Key::Num4 => Some('4'),
444 egui::Key::Num5 => Some('5'),
445 egui::Key::Num6 => Some('6'),
446 egui::Key::Num7 => Some('7'),
447 egui::Key::Num8 => Some('8'),
448 egui::Key::Num9 => Some('9'),
449 _ => None,
450 }
451}
452
453fn action_to_command(action: Action) -> Option<Command> {
456 match action {
457 Action::NextSlide => Some(Command::NextSlide),
458 Action::PreviousSlide => Some(Command::PreviousSlide),
459 Action::NextOverlay => Some(Command::NextOverlay),
460 Action::PreviousOverlay => Some(Command::PreviousOverlay),
461 Action::FirstSlide => Some(Command::FirstSlide),
462 Action::LastSlide => Some(Command::LastSlide),
463 Action::ToggleFreeze => Some(Command::ToggleFreeze),
464 Action::ToggleBlackout => Some(Command::ToggleBlackout),
465 Action::ToggleWhiteboard => Some(Command::ToggleWhiteboard),
466 Action::ToggleLaser => Some(Command::ToggleLaser),
467 Action::CycleLaserStyle => Some(Command::CycleLaserStyle),
468 Action::ToggleInk => Some(Command::ToggleInk),
469 Action::ClearInk => Some(Command::ClearInk),
470 Action::CycleInkColor => Some(Command::CycleInkColor),
471 Action::CycleInkWidth => Some(Command::CycleInkWidth),
472 Action::ToggleSpotlight => Some(Command::ToggleSpotlight),
473 Action::ToggleZoom => Some(Command::ToggleZoom),
474 Action::ToggleOverview => Some(Command::ToggleSlideOverview),
475 Action::ToggleNotes => Some(Command::ToggleNotesPanel),
476 Action::ToggleNotesEdit => Some(Command::ToggleNotesEdit),
477 Action::ResetTimer => Some(Command::ResetTimer),
478 Action::IncrementNotesFont => Some(Command::IncrementNotesFontSize),
479 Action::DecrementNotesFont => Some(Command::DecrementNotesFontSize),
480 Action::ToggleScreenShare => Some(Command::ToggleScreenShareMode),
481 Action::TogglePresentationMode => Some(Command::TogglePresentationMode),
482 Action::ToggleTextBoxMode => Some(Command::ToggleTextBoxMode),
483 Action::Quit => Some(Command::Quit),
484 Action::SaveSidecar => Some(Command::SaveSidecar),
485 Action::GoToSlide | Action::StartPauseTimer => None,
486 }
487}