winctx/window_loop/
clipboard_manager.rs1use std::str;
2
3use tokio::sync::mpsc::UnboundedSender;
4use windows_sys::Win32::Foundation::HWND;
5use windows_sys::Win32::UI::WindowsAndMessaging as winuser;
6use windows_sys::Win32::UI::WindowsAndMessaging::MSG;
7
8use crate::clipboard::{Clipboard, ClipboardFormat};
9use crate::error::{ErrorKind, WindowError};
10use crate::event::ClipboardEvent;
11use crate::Error;
12
13use super::WindowEvent;
14
15const CLIPBOARD_RETRY_TIMER: usize = 1000;
16const RETRY_MILLIS: u32 = 25;
17const RETRY_MAX_ATTEMPTS: usize = 10;
18
19const CLIPBOARD_DEBOUNCE_TIMER: usize = 1001;
23const DEBOUNCE_MILLIS: u32 = 25;
24
25pub(super) struct ClipboardManager<'a> {
27 events_tx: &'a UnboundedSender<WindowEvent>,
28 attempts: usize,
29 supported: Option<ClipboardFormat>,
30}
31
32impl<'a> ClipboardManager<'a> {
33 pub(super) fn new(events_tx: &'a UnboundedSender<WindowEvent>) -> Self {
34 Self {
35 events_tx,
36 attempts: 0,
37 supported: None,
38 }
39 }
40
41 pub(super) unsafe fn dispatch(&mut self, msg: &MSG) -> bool {
42 match msg.message {
43 winuser::WM_CLIPBOARDUPDATE => {
44 winuser::SetTimer(msg.hwnd, CLIPBOARD_DEBOUNCE_TIMER, DEBOUNCE_MILLIS, None);
46 true
47 }
48 winuser::WM_TIMER => match msg.wParam {
49 CLIPBOARD_RETRY_TIMER => {
50 self.handle_timer(msg.hwnd);
51 true
52 }
53 CLIPBOARD_DEBOUNCE_TIMER => {
54 winuser::KillTimer(msg.hwnd, CLIPBOARD_DEBOUNCE_TIMER);
55 self.populate_formats();
56
57 let Ok(result) = self.poll_clipboard(msg.hwnd) else {
74 winuser::SetTimer(msg.hwnd, CLIPBOARD_RETRY_TIMER, RETRY_MILLIS, None);
75 self.attempts = 1;
76 return true;
77 };
78
79 if let Some(clipboard_event) = result {
80 _ = self.events_tx.send(WindowEvent::Clipboard(clipboard_event));
81 }
82
83 true
84 }
85 _ => false,
86 },
87 _ => false,
88 }
89 }
90
91 fn populate_formats(&mut self) {
92 self.supported = 'out: {
93 for format in Clipboard::updated_formats::<16>() {
94 if matches!(
95 format,
96 ClipboardFormat::DIBV5 | ClipboardFormat::TEXT | ClipboardFormat::UNICODETEXT
97 ) {
98 break 'out Some(format);
99 }
100 }
101
102 None
103 };
104 }
105
106 unsafe fn handle_timer(&mut self, hwnd: HWND) {
107 let result = match self.poll_clipboard(hwnd) {
108 Ok(result) => result,
109 Err(error) => {
110 if self.attempts >= RETRY_MAX_ATTEMPTS {
111 winuser::KillTimer(hwnd, CLIPBOARD_RETRY_TIMER);
112 self.attempts = 0;
113 _ = self.events_tx.send(WindowEvent::Error(Error::new(
114 ErrorKind::ClipboardPoll(error),
115 )));
116 } else {
117 if self.attempts == 0 {
118 winuser::SetTimer(hwnd, CLIPBOARD_RETRY_TIMER, RETRY_MILLIS, None);
119 }
120
121 self.attempts += 1;
122 }
123
124 return;
125 }
126 };
127
128 winuser::KillTimer(hwnd, CLIPBOARD_RETRY_TIMER);
129 self.attempts = 0;
130
131 if let Some(clipboard_event) = result {
132 _ = self.events_tx.send(WindowEvent::Clipboard(clipboard_event));
133 }
134 }
135
136 pub(super) unsafe fn poll_clipboard(
137 &mut self,
138 hwnd: HWND,
139 ) -> Result<Option<ClipboardEvent>, WindowError> {
140 let clipboard = Clipboard::new(hwnd).map_err(WindowError::OpenClipboard)?;
141
142 let Some(format) = self.supported else {
143 return Ok(None);
144 };
145
146 let data = clipboard
147 .data(format)
148 .map_err(WindowError::GetClipboardData)?;
149 let data = data.lock().map_err(WindowError::LockClipboardData)?;
150
151 self.supported = None;
153
154 let clipboard_event = match format {
155 ClipboardFormat::DIBV5 => ClipboardEvent::BitMap(data.as_slice().to_vec()),
156 ClipboardFormat::TEXT => {
157 let data = data.as_slice();
158
159 let data = match data {
160 [head @ .., 0] => head,
161 rest => rest,
162 };
163
164 let Ok(string) = str::from_utf8(data) else {
165 return Ok(None);
166 };
167
168 ClipboardEvent::Text(string.to_owned())
169 }
170 ClipboardFormat::UNICODETEXT => {
171 let data = data.as_wide_slice();
172
173 let data = match data {
174 [head @ .., 0] => head,
175 rest => rest,
176 };
177
178 let Ok(string) = String::from_utf16(data) else {
179 return Ok(None);
180 };
181
182 ClipboardEvent::Text(string.to_owned())
183 }
184 _ => {
185 return Ok(None);
186 }
187 };
188
189 Ok(Some(clipboard_event))
190 }
191}