diff --git a/src/app/mod.rs b/src/app/mod.rs index 057ddd875..5bca593b2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2180,6 +2180,8 @@ pub enum Message { /// clipboard as plain text — issue #232, so output can be pasted for /// debugging instead of screenshotted. CommandHistoryCopy, + #[cfg(target_arch = "wasm32")] + CommandHistoryCopied(bool), /// Clear every line from the command-line history. CommandHistoryClear, /// Copy every line currently retained by the PERF panel. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index a76158d93..aeadd1654 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1839,10 +1839,32 @@ impl OpenCADStudio { Message::CommandHistoryCopy => { let text = self.command_line.history_plain_text(); if text.is_empty() { - Task::none() - } else { - iced::clipboard::write(text).discard() + return Task::none(); } + #[cfg(target_arch = "wasm32")] + { + let promise = crate::sys::copy_history_text( + &text, + crate::t!("Clipboard access is unavailable. Press Ctrl+C or Command+C to copy the selected history.").as_ref(), + crate::t!("Close").as_ref(), + ); + Task::perform(async move { + wasm_bindgen_futures::JsFuture::from(promise).await + .ok().and_then(|value| value.as_bool()).unwrap_or(false) + }, Message::CommandHistoryCopied) + } + #[cfg(not(target_arch = "wasm32"))] + iced::clipboard::write(text).discard() + } + + #[cfg(target_arch = "wasm32")] + Message::CommandHistoryCopied(copied) => { + if copied { + self.command_line.push_info(crate::t!("Copied").as_ref()); + } else { + self.command_line.push_error(crate::t!("Clipboard access is unavailable. Press Ctrl+C or Command+C to copy the selected history.").as_ref()); + } + Task::none() } Message::CommandHistoryClear => { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index aea6fcc41..3373e1aeb 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2930,7 +2930,7 @@ pub(super) fn doc_tab_bar<'a>( let new_btn = button(text("+").size(14)) .on_press(Message::TabNew) .height(iced::Length::Fixed(28.0)) - .padding([5, 10]) + .padding([4, 8]) .style(|theme: &Theme, status| { let palette = theme.palette(); let hovered = matches!( @@ -2960,7 +2960,7 @@ pub(super) fn doc_tab_bar<'a>( top: 0.0, right: 0.0, bottom: 0.0, - left: 6.0, + left: 2.0, }) .into(), ); diff --git a/src/sys.rs b/src/sys.rs index 7fecdf171..f53671640 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -226,6 +226,13 @@ pub async fn read_clipboard_text() -> Option { value.as_string() } +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen::prelude::wasm_bindgen(module = "/web/clipboard.js")] +extern "C" { + #[wasm_bindgen::prelude::wasm_bindgen(js_name = copyHistory)] + pub fn copy_history_text(text: &str, fallback_label: &str, close_label: &str) -> js_sys::Promise; +} + /// Web: write text to the system clipboard (fire-and-forget). Backs Ctrl+C in /// plain text_input fields, whose iced-internal copy is a no-op on the web /// (#346). The triggering keypress is a user gesture, so the write is allowed. diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 85a7660ab..7bf65623a 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -849,10 +849,10 @@ impl CommandLine { .align_y(iced::Center), ) .width(Length::Fill) - .padding([2, 6]); + .padding([2, 8]); let panel = container(column![header, log]) .width(Length::Fill) - .padding([4, 8]); + .padding(Padding { top: 2.0, right: 8.0, bottom: 4.0, left: 8.0 }); let resize = iced::widget::mouse_area( container(crate::ui::icons::themed_primary( crate::ui::icons::RESIZE, diff --git a/web/clipboard.js b/web/clipboard.js new file mode 100644 index 000000000..91ae11fb5 --- /dev/null +++ b/web/clipboard.js @@ -0,0 +1,25 @@ +export async function copyHistory(text, fallbackLabel, closeLabel) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + const dialog = document.createElement('dialog'); + const label = document.createElement('p'); + label.textContent = fallbackLabel; + const field = document.createElement('textarea'); + field.value = text; + field.readOnly = true; + field.setAttribute('aria-label', fallbackLabel); + field.style.cssText = 'width:70vw;height:50vh;display:block'; + const close = document.createElement('button'); + close.textContent = closeLabel; + close.onclick = () => dialog.close(); + dialog.append(label, field, close); + dialog.addEventListener('close', () => dialog.remove(), { once: true }); + document.body.append(dialog); + dialog.showModal(); + field.focus(); + field.select(); + return false; + } +}