Skip to content

Commit e23a437

Browse files
committed
fix copy to clipboard in wasm-version
1 parent 59b7650 commit e23a437

5 files changed

Lines changed: 136 additions & 5 deletions

File tree

Lines changed: 1 addition & 1 deletion
Loading

src/app.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ const POINTS_PARSE_DEBOUNCE_MS: u64 = 180;
115115
const POINTS_HISTORY_LIMIT: usize = 256;
116116
const POINTS_PARSE_ERROR_PREFIX: &str = "Points parse error: ";
117117
const CLIPBOARD_IMPORT_ERROR_PREFIX: &str = "Clipboard import error: ";
118+
#[cfg(target_arch = "wasm32")]
119+
const CLIPBOARD_COPY_ERROR_PREFIX: &str = "Clipboard copy error: ";
118120
#[cfg(not(target_arch = "wasm32"))]
119121
const CLIPBOARD_IMPORT_PASTE_TIMEOUT_MS: u64 = 1_500;
120122
const POINTS_POSITIVE_AXIS_EPS: f64 = 1e-6;
@@ -757,6 +759,10 @@ pub struct CurveFitApp {
757759
clipboard_import_web_in_flight: bool,
758760
#[cfg(target_arch = "wasm32")]
759761
clipboard_import_web_result: Rc<RefCell<Option<Result<String, String>>>>,
762+
#[cfg(target_arch = "wasm32")]
763+
clipboard_copy_web_in_flight: bool,
764+
#[cfg(target_arch = "wasm32")]
765+
clipboard_copy_web_result: Rc<RefCell<Option<Result<(), String>>>>,
760766
selected_model: ModelChoice,
761767
polynomial_degree: usize,
762768
parameter_inputs: Vec<String>,
@@ -1464,6 +1470,10 @@ impl Default for CurveFitApp {
14641470
clipboard_import_web_in_flight: false,
14651471
#[cfg(target_arch = "wasm32")]
14661472
clipboard_import_web_result: Rc::new(RefCell::new(None)),
1473+
#[cfg(target_arch = "wasm32")]
1474+
clipboard_copy_web_in_flight: false,
1475+
#[cfg(target_arch = "wasm32")]
1476+
clipboard_copy_web_result: Rc::new(RefCell::new(None)),
14671477
selected_model,
14681478
polynomial_degree,
14691479
parameter_inputs: params_to_input_strings(&selected_family.default_params()),
@@ -1559,6 +1569,7 @@ impl eframe::App for CurveFitApp {
15591569
self.maybe_run_pending_auto_refit();
15601570
self.tick_replay(ctx);
15611571
self.poll_points_clipboard_import(ctx);
1572+
self.poll_clipboard_copy(ctx);
15621573
#[cfg(not(target_arch = "wasm32"))]
15631574
self.poll_fit_export_save_dialog(ctx);
15641575
self.maybe_refresh_points_cache_after_debounce();

src/app/clipboard_import.rs

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,68 @@ impl CurveFitApp {
9393
}
9494
}
9595

96+
pub(super) fn copy_text_to_clipboard(&mut self, ctx: &egui::Context, text: String) {
97+
#[cfg(not(target_arch = "wasm32"))]
98+
{
99+
ctx.copy_text(text);
100+
}
101+
102+
#[cfg(target_arch = "wasm32")]
103+
{
104+
if self.clipboard_copy_web_in_flight {
105+
return;
106+
}
107+
108+
let promise = match start_write_text_to_web_clipboard(&text) {
109+
Ok(promise) => promise,
110+
Err(error) => {
111+
self.set_clipboard_copy_error(error);
112+
return;
113+
}
114+
};
115+
116+
let ctx = ctx.clone();
117+
let result_slot = Rc::clone(&self.clipboard_copy_web_result);
118+
self.clipboard_copy_web_in_flight = true;
119+
self.clipboard_copy_web_result.borrow_mut().take();
120+
wasm_bindgen_futures::spawn_local(async move {
121+
let result = wasm_bindgen_futures::JsFuture::from(promise)
122+
.await
123+
.map(|_| ())
124+
.map_err(|error| {
125+
format!(
126+
"Failed to write clipboard text: {}",
127+
describe_web_clipboard_js_error(&error)
128+
)
129+
});
130+
*result_slot.borrow_mut() = Some(result);
131+
ctx.request_repaint();
132+
});
133+
}
134+
}
135+
136+
pub(super) fn poll_clipboard_copy(&mut self, ctx: &egui::Context) {
137+
#[cfg(not(target_arch = "wasm32"))]
138+
let _ = ctx;
139+
140+
#[cfg(target_arch = "wasm32")]
141+
{
142+
if !self.clipboard_copy_web_in_flight {
143+
return;
144+
}
145+
146+
let maybe_result = self.clipboard_copy_web_result.borrow_mut().take();
147+
if let Some(result) = maybe_result {
148+
self.clipboard_copy_web_in_flight = false;
149+
if let Err(error) = result {
150+
self.set_clipboard_copy_error(error);
151+
}
152+
} else {
153+
ctx.request_repaint();
154+
}
155+
}
156+
}
157+
96158
pub(super) fn handle_points_clipboard_import_result(&mut self, result: Result<String, String>) {
97159
let text = match result {
98160
Ok(text) => text,
@@ -135,6 +197,18 @@ impl CurveFitApp {
135197
)));
136198
}
137199
}
200+
201+
#[cfg(target_arch = "wasm32")]
202+
fn set_clipboard_copy_error(&mut self, message: impl AsRef<str>) {
203+
let message = message.as_ref();
204+
if message.starts_with(CLIPBOARD_COPY_ERROR_PREFIX) {
205+
self.status = Some(StatusMessage::Error(message.to_owned()));
206+
} else {
207+
self.status = Some(StatusMessage::Error(format!(
208+
"{CLIPBOARD_COPY_ERROR_PREFIX}{message}"
209+
)));
210+
}
211+
}
138212
}
139213

140214
#[cfg(not(target_arch = "wasm32"))]
@@ -155,13 +229,59 @@ fn take_requested_paste_event(ctx: &egui::Context) -> Option<String> {
155229
async fn read_text_from_web_clipboard() -> Result<String, String> {
156230
use wasm_bindgen_futures::JsFuture;
157231

158-
let window = web_sys::window().ok_or_else(|| "Window is unavailable".to_string())?;
232+
let window = web_clipboard_window()?;
159233
let clipboard = window.navigator().clipboard();
160234
let text = JsFuture::from(clipboard.read_text())
161235
.await
162-
.map_err(|error| format!("Failed to read clipboard text: {error:?}"))?
236+
.map_err(|error| {
237+
format!(
238+
"Failed to read clipboard text: {}",
239+
describe_web_clipboard_js_error(&error)
240+
)
241+
})?
163242
.as_string()
164243
.ok_or_else(|| "Clipboard did not return text content".to_string())?;
165244

166245
Ok(text)
167246
}
247+
248+
#[cfg(target_arch = "wasm32")]
249+
fn start_write_text_to_web_clipboard(text: &str) -> Result<web_sys::js_sys::Promise, String> {
250+
// Для web Clipboard API запись должна стартовать прямо в обработчике user gesture.
251+
let window = web_clipboard_window()?;
252+
let clipboard = window.navigator().clipboard();
253+
Ok(clipboard.write_text(text))
254+
}
255+
256+
#[cfg(target_arch = "wasm32")]
257+
fn web_clipboard_window() -> Result<web_sys::Window, String> {
258+
let window = web_sys::window().ok_or_else(|| "Window is unavailable".to_string())?;
259+
if !window.is_secure_context() {
260+
return Err(
261+
"Clipboard API is unavailable in non-secure context (HTTPS or localhost required)"
262+
.to_string(),
263+
);
264+
}
265+
Ok(window)
266+
}
267+
268+
#[cfg(target_arch = "wasm32")]
269+
fn describe_web_clipboard_js_error(error: &wasm_bindgen::JsValue) -> String {
270+
if let Some(message) = error.as_string() {
271+
return message;
272+
}
273+
274+
let message = web_sys::js_sys::Reflect::get(error, &wasm_bindgen::JsValue::from_str("message"))
275+
.ok()
276+
.and_then(|value| value.as_string());
277+
let name = web_sys::js_sys::Reflect::get(error, &wasm_bindgen::JsValue::from_str("name"))
278+
.ok()
279+
.and_then(|value| value.as_string());
280+
281+
match (name, message) {
282+
(Some(name), Some(message)) => format!("{name}: {message}"),
283+
(None, Some(message)) => message,
284+
(Some(name), None) => name,
285+
(None, None) => format!("{error:?}"),
286+
}
287+
}

src/app/result_export.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ impl CurveFitApp {
109109
pub(super) fn copy_fit_export_json(&mut self, ctx: &egui::Context) {
110110
match self.build_fit_export_json_pretty() {
111111
Ok(json) => {
112-
ctx.copy_text(json);
112+
self.copy_text_to_clipboard(ctx, json);
113113
}
114114
Err(error) => {
115115
self.status = Some(StatusMessage::Error(error));

src/app/ui/formula_window.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub(super) fn ui_formula_window(app: &mut CurveFitApp, ctx: &egui::Context) {
2727
formula_window_hint,
2828
);
2929
if copy_response.clicked() {
30-
ui.ctx().copy_text(plain_formula.clone());
30+
app.copy_text_to_clipboard(ui.ctx(), plain_formula.clone());
3131
}
3232
});
3333
ui.add_space(4.0);

0 commit comments

Comments
 (0)