use crate::{
backend::Backend,
prelude::{CharFlags, ErrorKind, Key, KeyCode, KeyModifier, MouseButton, MouseWheelDirection, Size, Surface},
system::Error,
system::{KeyPressedEvent, MouseButtonDownEvent, MouseButtonUpEvent, MouseMoveEvent, MouseWheelEvent, SystemEvent},
};
use std::{
fmt::Write,
sync::{mpsc::Sender, Arc, Mutex},
};
use wasm_bindgen::{convert::FromWasmAbi, prelude::*, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
window, CanvasRenderingContext2d, EventTarget, HtmlCanvasElement, KeyboardEvent, MouseEvent, WebGlBuffer, WebGlProgram,
WebGlRenderingContext as GL, WheelEvent,
};
const CURSOR_COLOR: &str = "rgba(255, 255, 255, 0.5)";
const DEFAULT_COLOR: &str = "rgba(0, 0, 0, 0)";
struct TerminalDomConfig {
cols: u32,
rows: u32,
font_family: String,
font_size: u32,
cell_w: u32,
cell_h: u32,
}
struct WebGLResources {
gl: GL,
program: WebGlProgram,
buffer: WebGlBuffer,
pos_attrib_location: u32,
color_attrib_location: u32,
}
pub struct WebTerminal {
gl: GL,
size: Size,
webgl_canvas: HtmlCanvasElement,
text_canvas: HtmlCanvasElement,
program: WebGlProgram,
buffer: WebGlBuffer,
pos_attrib_location: u32,
color_attrib_location: u32,
event_queue: Arc<Mutex<Vec<SystemEvent>>>,
font: String,
cell_width_px: f32,
cell_height_px: f32,
clipboard_content: Arc<Mutex<Option<String>>>,
rgba_color: String,
}
unsafe impl Send for WebTerminal {}
unsafe impl Sync for WebTerminal {}
impl WebTerminal {
fn load_dom_config(document: &web_sys::Document) -> TerminalDomConfig {
let font_size_val = Self::get_config(document, "terminal-font-size", 20);
let font_family = Self::get_config(document, "terminal-font", "Consolas, \"Courier New\", monospace".to_string());
let cols_config = Self::get_config(document, "terminal-cols", 0);
let rows_config = Self::get_config(document, "terminal-rows", 0);
let cell_w_config = Self::get_config(document, "terminal-cell-width", 0);
let cell_h_config = Self::get_config(document, "terminal-cell-height", 0);
let (cols, rows, cell_w, cell_h) = if cols_config == 0 || rows_config == 0 || cell_w_config == 0 || cell_h_config == 0 {
Self::calculate_optimal_dimensions(&font_family, font_size_val)
} else {
(cols_config, rows_config, cell_w_config, cell_h_config)
};
TerminalDomConfig {
cols,
rows,
font_family,
font_size: font_size_val,
cell_w,
cell_h,
}
}
fn calculate_optimal_dimensions(font_family: &str, font_size: u32) -> (u32, u32, u32, u32) {
let window = match window() {
Some(w) => w,
None => {
let default_cell_w = (font_size as f64 * 0.6).ceil() as u32;
let default_cell_h = (font_size as f64 * 1.3).ceil() as u32;
return (80, 24, default_cell_w.max(1), default_cell_h.max(1));
}
};
let document = match window.document() {
Some(d) => d,
None => {
let default_cell_w = (font_size as f64 * 0.6).ceil() as u32;
let default_cell_h = (font_size as f64 * 1.3).ceil() as u32;
return (80, 24, default_cell_w.max(1), default_cell_h.max(1));
}
};
let canvas = document
.create_element("canvas")
.ok()
.and_then(|el| el.dyn_into::<HtmlCanvasElement>().ok());
if let Some(canvas) = canvas {
let context_result = canvas
.get_context("2d")
.ok()
.flatten()
.and_then(|ctx| ctx.dyn_into::<CanvasRenderingContext2d>().ok());
if let Some(context) = context_result {
let font_style = format!("{}px {}", font_size, font_family);
context.set_font(&font_style);
if let Ok(metrics) = context.measure_text("M") {
let char_width = metrics.width().ceil() as u32;
let cell_width = char_width.saturating_sub(1).max(1);
let cell_height = (font_size as f64 * 1.0).ceil() as u32;
let window_width = window.inner_width().ok().and_then(|w| w.as_f64()).unwrap_or(1920.0);
let window_height = window.inner_height().ok().and_then(|h| h.as_f64()).unwrap_or(1080.0);
let cols = if cell_width > 0 {
(window_width / cell_width as f64).floor() as u32
} else {
80
};
let rows = if cell_height > 0 {
(window_height / cell_height as f64).floor() as u32
} else {
24
};
let cols = cols.max(40);
let rows = rows.max(10);
let cols = cols + 1;
let rows = rows + 1;
return (cols, rows, cell_width, cell_height);
}
}
}
let default_cell_w = ((font_size as f64 * 0.6).ceil() as u32).saturating_sub(1).max(1);
let default_cell_h = ((font_size as f64 * 1.3).ceil() as u32).saturating_sub(2).max(1);
(80, 24, default_cell_w, default_cell_h)
}
fn initialize_canvases_from_dom(
document: &web_sys::Document,
config: &TerminalDomConfig,
) -> Result<(HtmlCanvasElement, HtmlCanvasElement), Error> {
let webgl_canvas = Self::get_canvas(document, "canvas")?;
let text_canvas = Self::get_canvas(document, "textCanvas")?;
Self::init_canvas(&webgl_canvas, config.cols, config.rows, config.cell_w, config.cell_h, 1)?;
Self::init_canvas(&text_canvas, config.cols, config.rows, config.cell_w, config.cell_h, 2)?;
Ok((webgl_canvas, text_canvas))
}
fn initialize_webgl_resources(webgl_canvas: &HtmlCanvasElement, config: &TerminalDomConfig) -> Result<WebGLResources, Error> {
let gl = Self::get_gl(webgl_canvas)?;
gl.enable(GL::BLEND);
gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA);
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.clear(GL::COLOR_BUFFER_BIT);
gl.viewport(0, 0, webgl_canvas.width() as i32, webgl_canvas.height() as i32);
let program = Self::init_program(&gl)?;
let pos_attrib_location = gl.get_attrib_location(&program, "position") as u32;
let color_attrib_location = gl.get_attrib_location(&program, "color") as u32;
let buffer = gl
.create_buffer()
.ok_or_else(|| Error::new(ErrorKind::InitializationFailure, "Failed to create WebGL buffer".into()))?;
Ok(WebGLResources {
gl,
program,
buffer,
pos_attrib_location,
color_attrib_location,
})
}
pub(crate) fn new(builder: &crate::system::Builder, sender: Sender<SystemEvent>) -> Result<Self, Error> {
let document = Self::document()?;
document.set_title(builder.title.as_deref().unwrap_or("AppCUI Web Terminal"));
let dom_config = Self::load_dom_config(&document);
let (webgl_canvas, text_canvas) = Self::initialize_canvases_from_dom(&document, &dom_config)?;
let webgl_resources = Self::initialize_webgl_resources(&webgl_canvas, &dom_config)?;
let event_queue = Arc::new(Mutex::new(Vec::new()));
let font_style = format!("{}px {}", dom_config.font_size, dom_config.font_family);
let term = WebTerminal {
gl: webgl_resources.gl,
size: Size {
width: dom_config.cols,
height: dom_config.rows,
},
webgl_canvas,
text_canvas,
program: webgl_resources.program,
buffer: webgl_resources.buffer,
pos_attrib_location: webgl_resources.pos_attrib_location,
color_attrib_location: webgl_resources.color_attrib_location,
event_queue: event_queue.clone(),
font: font_style,
cell_width_px: dom_config.cell_w as f32,
cell_height_px: dom_config.cell_h as f32,
clipboard_content: Arc::new(Mutex::new(None)),
rgba_color: String::with_capacity(32),
};
term.setup_input_listeners(&document, sender)
.map_err(|e| Error::new(ErrorKind::InitializationFailure, format!("Failed to initialize input listeners: {:?}", e)))?;
Ok(term)
}
fn get_config<T: std::str::FromStr>(document: &web_sys::Document, id: &str, default: T) -> T {
document
.get_element_by_id(id)
.and_then(|el| el.text_content())
.and_then(|s| s.parse::<T>().ok())
.unwrap_or(default)
}
fn init_canvas(canvas: &HtmlCanvasElement, cols: u32, rows: u32, cell_w: u32, cell_h: u32, z_index: u32) -> Result<(), Error> {
let window = window().ok_or_else(|| Error::new(ErrorKind::InitializationFailure, "Window not available".into()))?;
let window_width = window.inner_width().ok().and_then(|w| w.as_f64()).unwrap_or((cols * cell_w) as f64) as u32;
let window_height = window.inner_height().ok().and_then(|h| h.as_f64()).unwrap_or((rows * cell_h) as f64) as u32;
let width = window_width;
let height = window_height;
canvas.set_width(width);
canvas.set_height(height);
let style = canvas.style();
for &(prop, ref val) in &[
("width", format!("{}px", width)),
("height", format!("{}px", height)),
("background-color", "transparent".into()),
("position", "absolute".into()),
("z-index", z_index.to_string()),
] {
style
.set_property(prop, val)
.map_err(|e| Error::new(ErrorKind::InitializationFailure, format!("Failed to set {}: {:?}", prop, e)))?;
}
Ok(())
}
fn document() -> Result<web_sys::Document, Error> {
window()
.ok_or_else(|| Error::new(ErrorKind::InitializationFailure, "No window found".into()))?
.document()
.ok_or_else(|| Error::new(ErrorKind::InitializationFailure, "No document found".into()))
}
fn get_canvas(document: &web_sys::Document, id: &str) -> Result<HtmlCanvasElement, Error> {
document
.get_element_by_id(id)
.ok_or_else(|| Error::new(ErrorKind::InitializationFailure, format!("No element with id '{}'", id)))?
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| Error::new(ErrorKind::InitializationFailure, format!("Element '{}' is not a canvas", id)))
}
fn get_gl(canvas: &HtmlCanvasElement) -> Result<GL, Error> {
let opts = js_sys::Object::new();
js_sys::Reflect::set(&opts, &"alpha".into(), &true.into()).map_err(|e| {
Error::new(
ErrorKind::InitializationFailure,
format!("Failed to configure WebGL context options: {e:?}"),
)
})?;
canvas
.get_context_with_context_options("webgl", &opts)
.map_err(|e| Error::new(ErrorKind::InitializationFailure, format!("Error getting WebGL context: {e:?}")))?
.ok_or_else(|| Error::new(ErrorKind::InitializationFailure, "WebGL not supported".into()))?
.dyn_into::<GL>()
.map_err(|e| Error::new(ErrorKind::InitializationFailure, format!("Failed to cast context: {:?}", e)))
}
fn key_index(k: &str) -> u8 {
match k {
"F1" => 1,
"F2" => 2,
"F3" => 3,
"F4" => 4,
"F5" => 5,
"F6" => 6,
"F7" => 7,
"F8" => 8,
"F9" => 9,
"F10" => 10,
"F11" => 11,
"F12" => 12,
"Enter" => 13,
"Escape" => 14,
"Insert" => 15,
"Delete" => 16,
"Backspace" => 17,
"Tab" => 18,
"ArrowLeft" | "Left" => 19,
"ArrowUp" | "Up" => 20,
"ArrowDown" | "Down" => 21,
"ArrowRight" | "Right" => 22,
"PageUp" => 23,
"PageDown" => 24,
"Home" => 25,
"End" => 26,
" " | "Space" | "Spacebar" => 27,
_ => {
if let Some(ch) = k.chars().next().filter(|_| k.len() == 1) {
match ch {
'A'..='Z' => ch as u8 - b'A' + 28,
'a'..='z' => ch.to_ascii_uppercase() as u8 - b'A' + 28,
'0'..='9' => ch as u8 - b'0' + 54,
_ => 0,
}
} else {
0
}
}
}
}
fn create_mouse_event_handler<F>(&self, sender: Sender<SystemEvent>, event_mapper: F) -> Closure<dyn FnMut(MouseEvent)>
where
F: Fn(i32, i32, &MouseEvent) -> SystemEvent + 'static,
{
let queue = self.event_queue.clone();
let canvas = self.webgl_canvas.clone();
let cw = self.cell_width_px;
let ch = self.cell_height_px;
Closure::wrap(Box::new(move |event: MouseEvent| {
let r = canvas.get_bounding_client_rect();
let x = ((event.client_x() as f32 - r.left() as f32) / cw) as i32;
let y = ((event.client_y() as f32 - r.top() as f32) / ch) as i32;
let system_event = event_mapper(x, y, &event);
if sender.send(system_event.clone()).is_ok() {
if let Ok(mut q) = queue.lock() {
q.push(system_event);
}
}
}))
}
fn create_wheel_event_handler<F>(&self, sender: Sender<SystemEvent>, event_mapper: F) -> Closure<dyn FnMut(WheelEvent)>
where
F: Fn(i32, i32, &WheelEvent) -> SystemEvent + 'static,
{
let queue = self.event_queue.clone();
let canvas = self.webgl_canvas.clone();
let cw = self.cell_width_px;
let ch = self.cell_height_px;
Closure::wrap(Box::new(move |event: WheelEvent| {
let r = canvas.get_bounding_client_rect();
let x = ((event.client_x() as f32 - r.left() as f32) / cw) as i32;
let y = ((event.client_y() as f32 - r.top() as f32) / ch) as i32;
let system_event = event_mapper(x, y, &event);
if sender.send(system_event.clone()).is_ok() {
if let Ok(mut q) = queue.lock() {
q.push(system_event);
}
}
event.prevent_default();
}))
}
fn setup_input_listeners(&self, document: &web_sys::Document, sender: Sender<SystemEvent>) -> Result<(), JsValue> {
let target: &EventTarget = document.as_ref();
fn attach<E: 'static + JsCast>(target: &EventTarget, event_name: &str, c: Closure<dyn FnMut(E)>) -> Result<(), JsValue>
where
E: FromWasmAbi,
{
target.add_event_listener_with_callback(event_name, c.as_ref().unchecked_ref())?;
c.forget();
Ok(())
}
{
let keyboard_sender = sender.clone();
let queue = self.event_queue.clone();
let key_closure: Closure<dyn FnMut(KeyboardEvent)> = Closure::wrap(Box::new(move |event| {
let k = event.key();
let idx = WebTerminal::key_index(&k);
if (1..=10).contains(&idx) {
event.prevent_default();
}
let key_code = KeyCode::from(idx);
let mut mods = KeyModifier::None;
if event.alt_key() {
mods |= KeyModifier::Alt;
}
if event.ctrl_key() {
mods |= KeyModifier::Ctrl;
}
if event.shift_key() {
mods |= KeyModifier::Shift;
}
let character = if k.len() == 1 { k.chars().next().unwrap_or('\0') } else { '\0' };
let sys_event = SystemEvent::KeyPressed(KeyPressedEvent {
key: Key::new(key_code, mods),
character,
});
if keyboard_sender.send(sys_event.clone()).is_ok() {
if let Ok(mut q) = queue.lock() {
q.push(sys_event);
}
}
}));
attach::<KeyboardEvent>(target, "keydown", key_closure)?;
}
let mouse_move_closure = self.create_mouse_event_handler(sender.clone(), |x, y, _event| {
SystemEvent::MouseMove(MouseMoveEvent {
x,
y,
button: MouseButton::None,
})
});
attach::<MouseEvent>(target, "mousemove", mouse_move_closure)?;
let mouse_down_closure = self.create_mouse_event_handler(sender.clone(), |x, y, event| {
let button = match event.button() {
0 => MouseButton::Left,
1 => MouseButton::Center,
2 => MouseButton::Right,
_ => MouseButton::None,
};
SystemEvent::MouseButtonDown(MouseButtonDownEvent { x, y, button })
});
attach::<MouseEvent>(target, "mousedown", mouse_down_closure)?;
let mouse_up_closure = self.create_mouse_event_handler(sender.clone(), |x, y, event| {
let button = match event.button() {
0 => crate::input::MouseButton::Left,
1 => crate::input::MouseButton::Center,
2 => crate::input::MouseButton::Right,
_ => crate::input::MouseButton::None,
};
SystemEvent::MouseButtonUp(MouseButtonUpEvent { x, y, button })
});
attach::<MouseEvent>(target, "mouseup", mouse_up_closure)?;
let wheel_closure = self.create_wheel_event_handler(sender, |x, y, event| {
let direction = if event.delta_y() < 0.0 {
MouseWheelDirection::Up
} else {
MouseWheelDirection::Down
};
SystemEvent::MouseWheel(MouseWheelEvent { x, y, direction })
});
attach::<WheelEvent>(target, "wheel", wheel_closure)?;
Ok(())
}
fn render_background(&self, surface: &Surface) {
let canvas_width = self.webgl_canvas.width() as f32;
let canvas_height = self.webgl_canvas.height() as f32;
let cell_width = self.cell_width_px;
let cell_height = self.cell_height_px;
let width = surface.size.width as i32;
let height = surface.size.height as i32;
let mut vertices: Vec<f32> = Vec::new();
for global_y in 0..height {
for global_x in 0..width {
if let Some(cell) = &surface.chars.get(global_y as usize * width as usize + global_x as usize) {
let pos_x = global_x as f32 * cell_width;
let pos_y = global_y as f32 * cell_height;
let bg_color = self.color_to_rgba(cell.background);
let bg_color = [bg_color[0] / 255.0, bg_color[1] / 255.0, bg_color[2] / 255.0, bg_color[3]];
let x_ndc = 2.0 * (pos_x / canvas_width) - 1.0;
let y_ndc = 1.0 - 2.0 * (pos_y / canvas_height);
let w_ndc = 2.0 * (cell_width / canvas_width);
let h_ndc = 2.0 * (cell_height / canvas_height);
vertices.extend_from_slice(&[
x_ndc,
y_ndc,
bg_color[0],
bg_color[1],
bg_color[2],
bg_color[3],
x_ndc,
y_ndc - h_ndc,
bg_color[0],
bg_color[1],
bg_color[2],
bg_color[3],
x_ndc + w_ndc,
y_ndc - h_ndc,
bg_color[0],
bg_color[1],
bg_color[2],
bg_color[3],
]);
vertices.extend_from_slice(&[
x_ndc,
y_ndc,
bg_color[0],
bg_color[1],
bg_color[2],
bg_color[3],
x_ndc + w_ndc,
y_ndc - h_ndc,
bg_color[0],
bg_color[1],
bg_color[2],
bg_color[3],
x_ndc + w_ndc,
y_ndc,
bg_color[0],
bg_color[1],
bg_color[2],
bg_color[3],
]);
}
}
}
self.gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.buffer));
unsafe {
let vertex_array = js_sys::Float32Array::view(&vertices);
self.gl
.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &vertex_array, GL::DYNAMIC_DRAW);
}
self.gl.use_program(Some(&self.program));
let stride = (6 * std::mem::size_of::<f32>()) as i32;
self.gl.enable_vertex_attrib_array(self.pos_attrib_location);
self.gl
.vertex_attrib_pointer_with_i32(self.pos_attrib_location, 2, GL::FLOAT, false, stride, 0);
self.gl.enable_vertex_attrib_array(self.color_attrib_location);
self.gl.vertex_attrib_pointer_with_i32(
self.color_attrib_location,
4,
GL::FLOAT,
false,
stride,
(2 * std::mem::size_of::<f32>()) as i32,
);
let vertex_count = (vertices.len() / 6) as i32;
self.gl.draw_arrays(GL::TRIANGLES, 0, vertex_count);
}
fn render_text(&mut self, surface: &Surface) -> Result<(), JsValue> {
let context = self
.text_canvas
.get_context("2d")?
.ok_or("2d context not available")?
.dyn_into::<CanvasRenderingContext2d>()?;
let canvas_width = self.text_canvas.width() as f64;
let canvas_height = self.text_canvas.height() as f64;
let cell_width = self.cell_width_px as f64;
let cell_height = self.cell_height_px as f64;
context.clear_rect(0.0, 0.0, canvas_width, canvas_height);
context.save();
context.set_font(self.font.as_str());
context.set_text_baseline("top");
context.set_text_align("center");
let width = surface.size.width as usize;
let height = surface.size.height as usize;
for y in 0..height {
let mut x = 0;
while x < width {
let index = y * width + x;
let Some(cell) = surface.chars.get(index) else {
x += 1;
continue;
};
let char_width_cells = self.is_emoji(cell.code) as usize + 1;
let pos_x = x as f64 * cell_width;
let pos_y = y as f64 * cell_height;
let foreground = self.color_to_rgba(cell.foreground);
self.rgba_color.clear();
write!(
&mut self.rgba_color,
"rgba({}, {}, {}, {})",
foreground[0], foreground[1], foreground[2], foreground[3]
)
.unwrap_or_else(|_| {
self.rgba_color = DEFAULT_COLOR.to_string();
});
context.set_fill_style_str(&self.rgba_color);
context.set_stroke_style_str(&self.rgba_color);
let render_center_x = pos_x + (char_width_cells as f64 * cell_width) / 2.0;
context.fill_text(&cell.code.to_string(), render_center_x, pos_y)?;
if cell.flags.contains(CharFlags::Underline) {
context.begin_path();
let x_start = pos_x + 1.0;
let underline_width_px = char_width_cells as f64 * cell_width;
let x_end = pos_x + underline_width_px - 1.0;
let y_line = pos_y + cell_height - 1.0;
context.move_to(x_start, y_line);
context.line_to(x_end, y_line);
context.stroke();
}
x += char_width_cells;
}
}
context.restore();
Ok(())
}
fn is_emoji(&self, c: char) -> bool {
matches!(
c as u32,
0x2600..=0x27BF | 0x1F300..=0x1F5FF | 0x1F600..=0x1F64F | 0x1F900..=0x1F9FF | 0x2100..=0x214F | 0x2300..=0x23FF | 0x2B00..=0x2BFF | 0x1F1E6..=0x1F1FF )
}
fn render_cursor(&self, surface: &Surface) -> Result<(), JsValue> {
if !surface.cursor.is_visible() {
return Ok(());
}
let context = self
.text_canvas
.get_context("2d")?
.ok_or("2d context not available")?
.dyn_into::<CanvasRenderingContext2d>()?;
let canvas_width = self.text_canvas.width() as f64;
let canvas_height = self.text_canvas.height() as f64;
let num_cols = surface.size.width as f64;
let num_rows = surface.size.height as f64;
let cell_width = canvas_width / num_cols;
let cell_height = canvas_height / num_rows;
let cursor_x = surface.cursor.x as f64;
let cursor_y = surface.cursor.y as f64;
context.set_fill_style_str(CURSOR_COLOR);
context.fill_rect(cursor_x * cell_width, cursor_y * cell_height, cell_width, cell_height);
Ok(())
}
}
impl Backend for WebTerminal {
fn on_resize(&mut self, new_size: Size) {
self.webgl_canvas.set_width(new_size.width);
self.webgl_canvas.set_height(new_size.height);
self.text_canvas.set_width(new_size.width);
self.text_canvas.set_height(new_size.height);
self.gl.viewport(0, 0, new_size.width as i32, new_size.height as i32);
}
fn is_single_threaded(&self) -> bool {
false
}
fn update_screen(&mut self, surface: &Surface) {
self.render_background(surface);
if let Err(e) = self.render_text(surface) {
web_sys::console::log_1(&format!("Error rendering text: {e:?}").into());
}
if let Err(e) = self.render_cursor(surface) {
web_sys::console::log_1(&format!("Error rendering cursor: {e:?}").into());
}
}
fn size(&self) -> Size {
self.size
}
fn clipboard_text(&self) -> Option<String> {
self.clipboard_content.lock().unwrap().clone()
}
fn has_clipboard_text(&self) -> bool {
self.clipboard_content.lock().unwrap().is_some()
}
fn query_system_event(&mut self) -> Option<SystemEvent> {
let mut queue = self.event_queue.lock().unwrap();
if !queue.is_empty() {
Some(queue.remove(0))
} else {
None
}
}
fn set_clipboard_text(&mut self, text: &str) {
let text_is_empty = text.is_empty();
{
let mut local_clipboard = self.clipboard_content.lock().unwrap();
if text_is_empty {
*local_clipboard = None;
} else {
*local_clipboard = Some(text.to_string());
}
}
if !text_is_empty {
if let Some(window) = web_sys::window() {
let clipboard = window.navigator().clipboard();
let text_owned = text.to_string();
let promise = clipboard.write_text(&text_owned);
wasm_bindgen_futures::spawn_local(async move {
match JsFuture::from(promise).await {
Ok(_) => {
web_sys::console::log_1(&format!("Successfully wrote to browser clipboard: '{text_owned}'").into());
}
Err(e) => {
web_sys::console::error_1(&format!("Failed to write to browser clipboard: {e:?}").into());
}
}
});
} else {
web_sys::console::warn_1(&"Window object not available for clipboard operation.".into());
}
}
}
}