Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src-tauri/src/display_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,33 @@ pub fn target_center(
))
}

pub fn cycle_center(
cursor: (f64, f64),
displays: &[Display],
next: bool,
) -> Result<(i32, i32), String> {
let current = current_display(cursor, displays).ok_or("No scanning display is available.")?;
if displays.len() < 2 {
return Err("No other display is connected.".into());
}
let mut ordered = displays.iter().collect::<Vec<_>>();
ordered.sort_by_key(|d| (d.x, d.y, d.width, d.height));
let index = ordered
.iter()
.position(|d| *d == current)
.ok_or("Display is no longer available.")?;
let index = if next {
(index + 1) % ordered.len()
} else {
(index + ordered.len() - 1) % ordered.len()
};
let (x, y) = ordered[index].center();
Ok((
x.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
y.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
))
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -327,6 +354,28 @@ mod tests {
}
}

#[test]
fn display_cycle_is_stable_wraps_and_handles_missing_displays() {
let displays = vec![
display(1920, 0, 1920, 1080),
display(0, 0, 1920, 1080),
display(-1280, 0, 1280, 1024),
];
assert_eq!(
cycle_center((10., 10.), &displays, true).unwrap(),
(2880, 540)
);
assert_eq!(
cycle_center((10., 10.), &displays, false).unwrap(),
(-640, 512)
);
assert_eq!(
cycle_center((2000., 10.), &displays, true).unwrap(),
(-640, 512)
);
assert!(cycle_center((0., 0.), &displays[..1], true).is_err());
assert!(cycle_center((0., 0.), &[], true).is_err());
}
#[test]
fn finds_each_direction_and_centers_mixed_resolutions() {
let source = display(0, 0, 1920, 1080);
Expand Down
80 changes: 80 additions & 0 deletions src-tauri/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ use crate::mouse_repeat::RepeatKey;
use crate::protocol::MouseButton;
use crate::state::{normalize_pointer_scale_percent, AppModel, SwitchBinding, SwitchProfile};

pub const SCAN_EVENT_MARKER: i64 = 0x53575043;
pub fn injection_settings() -> enigo::Settings {
enigo::Settings {
event_source_user_data: Some(SCAN_EVENT_MARKER),
windows_dw_extra_info: Some(SCAN_EVENT_MARKER as usize),
..Default::default()
}
}
pub fn own_input(marker: i64) -> bool {
marker == SCAN_EVENT_MARKER
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerFeedback {
Move,
Expand Down Expand Up @@ -132,6 +144,10 @@ fn pointer_button(button: MouseButton) -> Button {
#[cfg(target_os = "windows")]
fn windows_alphanumeric_key(name: &str) -> Option<Key> {
Some(match name {
"=" => Key::OEMPlus,
"-" => Key::OEMMinus,
"[" => Key::OEM4,
"]" => Key::OEM6,
"A" => Key::A,
"B" => Key::B,
"C" => Key::C,
Expand Down Expand Up @@ -400,6 +416,29 @@ impl InputInjector for Enigo {
.map_err(enigo_error("send the media command"))
}
fn window(&mut self, action: &str) -> Result<(), String> {
#[cfg(target_os = "macos")]
if action == "closeFocused" {
return crate::scan_host::close_foreground_window();
}
#[cfg(target_os = "windows")]
if matches!(action, "minimizeFocused" | "maximizeFocused") {
use windows_sys::Win32::UI::WindowsAndMessaging::*;
let window = unsafe { GetForegroundWindow() };
if window.is_null() {
return Err("No foreground window.".into());
}
let command = if action == "minimizeFocused" {
SW_MINIMIZE
} else if unsafe { IsZoomed(window) } != 0 {
SW_RESTORE
} else {
SW_MAXIMIZE
};
unsafe {
ShowWindow(window, command);
}
return Ok(());
}
#[cfg(target_os = "windows")]
let keys: &[&str] = match action {
"switchNext" => &["Alt", "Tab"],
Expand Down Expand Up @@ -433,6 +472,7 @@ impl InputInjector for Enigo {

pub struct DesktopInput<I: InputInjector> {
pub(crate) injector: I,
scan_keys: Vec<String>,
held_modifiers: HashSet<ModifierKey>,
pending_modifier_releases: HashSet<ModifierKey>,
pending_key_releases: HashSet<RepeatKey>,
Expand Down Expand Up @@ -462,6 +502,7 @@ impl<I: InputInjector> DesktopInput<I> {
pub fn new(injector: I) -> Self {
Self {
injector,
scan_keys: vec![],
held_modifiers: HashSet::new(),
pending_modifier_releases: HashSet::new(),
pending_key_releases: HashSet::new(),
Expand Down Expand Up @@ -1233,9 +1274,48 @@ impl<I: InputInjector> DesktopInput<I> {
}
}

pub fn scan_chord(
&mut self,
keys: &[&str],
click: Option<((i32, i32), MouseButton, u8)>,
) -> Result<(), String> {
self.release_scan_keys()?;
let result = (|| {
for key in keys {
self.scan_keys.push((*key).into());
self.injector.set_key(key, true)?;
}
if let Some((point, button, count)) = click {
self.move_pointer_absolute(point.0, point.1)?;
self.held_button = Some(button);
self.injector.click_pointer(button, count)?;
self.held_button = None;
}
Ok(())
})();
let released = self.release_scan_keys();
result.and(released)
}
fn release_scan_keys(&mut self) -> Result<(), String> {
let mut error = None;
for index in (0..self.scan_keys.len()).rev() {
match self.injector.set_key(&self.scan_keys[index], false) {
Ok(()) => {
self.scan_keys.remove(index);
}
Err(e) => {
error.get_or_insert(e);
}
}
}
error.map_or(Ok(()), Err)
}
pub fn release_all(&mut self) -> Result<(), String> {
let mut first_error = self.stop_switch_session().err();
self.text_streams.clear();
if let Err(error) = self.release_scan_keys() {
first_error.get_or_insert(error);
}
if let Err(error) = self.release_held_button() {
first_error.get_or_insert(error);
}
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ impl AccessibilityAdapter for SystemAccessibilityAdapter {
fn create_input(&mut self) -> Result<Self::Input, String> {
Enigo::new(&Settings {
open_prompt_to_get_permissions: false,
..Settings::default()
..crate::input::injection_settings()
})
.map_err(|error| error.to_string())
}

fn request_prompt(&mut self) {
let _ = Enigo::new(&Settings {
open_prompt_to_get_permissions: true,
..Settings::default()
..crate::input::injection_settings()
});
}

Expand Down
20 changes: 16 additions & 4 deletions src-tauri/src/modifier_overlay_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use windows::Win32::Graphics::Gdi::{
GetMonitorInfoW, MonitorFromPoint, ReleaseDC, SelectObject, SetBkMode, SetTextColor,
AC_SRC_ALPHA, AC_SRC_OVER, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, BLENDFUNCTION,
CLIP_DEFAULT_PRECIS, DEFAULT_CHARSET, DEFAULT_QUALITY, DIB_RGB_COLORS, DT_CENTER,
DT_SINGLELINE, DT_VCENTER, FF_DONTCARE, FW_BOLD, HDC, HGDIOBJ, MONITORINFO,
DT_SINGLELINE, DT_VCENTER, DT_WORDBREAK, FF_DONTCARE, FW_BOLD, HDC, HGDIOBJ, MONITORINFO,
MONITOR_DEFAULTTONEAREST, OUT_DEFAULT_PRECIS, TRANSPARENT,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
Expand Down Expand Up @@ -405,7 +405,13 @@ fn present(window: HWND, labels: &[String], layout: &Layout) -> Result<(), Strin
None,
);
}
present_pixmap_with_text(window, labels, layout, pixmap.data())
present_pixmap_with_text(
window,
labels,
layout,
pixmap.data(),
labels.iter().any(|label| label.contains('\n')),
)
}

fn rounded_rect(rect: Rect, radius: f32) -> Result<tiny_skia::Path, String> {
Expand Down Expand Up @@ -459,6 +465,7 @@ fn present_pixmap_with_text(
labels: &[String],
layout: &Layout,
rgba: &[u8],
wrap: bool,
) -> Result<(), String> {
unsafe {
let screen = ScreenDc(GetDC(None));
Expand Down Expand Up @@ -533,7 +540,11 @@ fn present_pixmap_with_text(
memory.0,
&mut text,
&mut text_rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
if wrap {
DT_CENTER | DT_WORDBREAK
} else {
DT_CENTER | DT_VCENTER | DT_SINGLELINE
},
);
}

Expand Down Expand Up @@ -603,6 +614,7 @@ pub(crate) fn present_scan_tile(
std::slice::from_ref(&tile.text),
&layout,
pixmap.data(),
true,
)
}

Expand All @@ -615,7 +627,7 @@ pub(crate) fn present_scan_prompt(
scale: f64,
) -> Result<(), String> {
let padding = (12.0 * scale).round() as i32;
let height = (64.0 * scale).round() as i32;
let height = (if text.contains('\n') { 154.0 } else { 64.0 } * scale).round() as i32;
let layout = Layout {
x,
y,
Expand Down
6 changes: 5 additions & 1 deletion src-tauri/src/point_scan_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ impl Adapter for PointScan {
}
fn activate(app: &AppHandle, request: Request) -> Result<(), String> {
crate::point_scan_ready(app)?;
crate::scan_executor::activate(request)
match request {
Request::Setting(setting) => scanning_runtime::update_point_setting(app, setting),
Request::Display(next) => scanning_runtime::restart_point_on_display(app, next),
request => crate::scan_executor::activate(request),
}
}
fn cleanup(_app: &AppHandle) -> Result<(), String> {
crate::scan_executor::cleanup()
Expand Down
Loading