servo 0.1.0

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use servo_base::generic_channel::GenericCallback;

use crate::WebView;

pub struct StringRequest {
    pub(crate) result_sender: GenericCallback<Result<String, String>>,
    response_sent: bool,
}

impl StringRequest {
    pub fn success(mut self, string: String) {
        let _ = self.result_sender.send(Ok(string));
        self.response_sent = true;
    }

    pub fn failure(mut self, message: String) {
        let _ = self.result_sender.send(Err(message));
        self.response_sent = true;
    }
}

impl From<GenericCallback<Result<String, String>>> for StringRequest {
    fn from(result_sender: GenericCallback<Result<String, String>>) -> Self {
        Self {
            result_sender,
            response_sent: false,
        }
    }
}

impl Drop for StringRequest {
    fn drop(&mut self) {
        if !self.response_sent {
            let _ = self
                .result_sender
                .send(Err("No response sent to request.".into()));
        }
    }
}

/// A delegate that is responsible for accessing the system clipboard. On Mac, Windows, and
/// Linux if the `clipboard` feature is enabled, a default delegate is automatically used
/// that implements clipboard support. An embedding application can override this delegate
/// by using this trait.
pub trait ClipboardDelegate {
    /// A request to clear all contents of the system clipboard.
    fn clear(&self, _webview: WebView) {}

    /// A request to get the text contents of the system clipboard. Once the contents are
    /// retrieved the embedder should call [`StringRequest::success`] with the text or
    /// [`StringRequest::failure`] with a failure message.
    fn get_text(&self, _webview: WebView, _request: StringRequest) {}

    /// A request to set the text contents of the system clipboard to `new_contents`.
    fn set_text(&self, _webview: WebView, _new_contents: String) {}
}

pub(crate) struct DefaultClipboardDelegate;

impl ClipboardDelegate for DefaultClipboardDelegate {
    fn clear(&self, _webview: WebView) {
        clipboard::clear();
    }

    fn get_text(&self, _webview: WebView, request: StringRequest) {
        clipboard::get_text(request);
    }

    fn set_text(&self, _webview: WebView, new_contents: String) {
        clipboard::set_text(new_contents);
    }
}

mod fallback_clipboard {
    use std::sync::{LockResult, Mutex, OnceLock};

    use crate::clipboard_delegate::StringRequest;

    /// If the clipboard cannot be accessed, we fall back to a simple `String` to store
    /// text for the clipboard. This obviously does not work across processes.
    static SHARED_FALLBACK_CLIPBOARD: OnceLock<Mutex<String>> = OnceLock::new();

    fn with_shared_clipboard(callback: impl FnOnce(&mut String)) {
        let clipboard_mutex =
            SHARED_FALLBACK_CLIPBOARD.get_or_init(|| Mutex::new(Default::default()));
        if let LockResult::Ok(mut string) = clipboard_mutex.lock() {
            callback(&mut string)
        }
    }

    pub(super) fn clear() {
        with_shared_clipboard(|clipboard_string| {
            clipboard_string.clear();
        });
    }

    pub(super) fn get_text(request: StringRequest) {
        with_shared_clipboard(move |clipboard_string| request.success(clipboard_string.clone()));
    }

    pub(super) fn set_text(new_contents: String) {
        with_shared_clipboard(move |clipboard_string| {
            *clipboard_string = new_contents;
        });
    }
}

#[cfg(all(
    feature = "clipboard",
    not(any(target_os = "android", target_env = "ohos"))
))]
mod clipboard {
    use std::sync::OnceLock;

    use arboard::Clipboard;
    use parking_lot::Mutex;

    use super::StringRequest;
    use crate::clipboard_delegate::fallback_clipboard;

    /// A shared clipboard for use by the [`DefaultClipboardDelegate`]. This is protected by
    /// a mutex so that it can only be used by one thread at a time. The `arboard` documentation
    /// suggests that more than one thread shouldn't try to access the Windows clipboard at a
    /// time. See <https://docs.rs/arboard/latest/arboard/struct.Clipboard.html>.
    static SHARED_CLIPBOARD: OnceLock<Option<Mutex<Clipboard>>> = OnceLock::new();

    fn with_shared_clipboard<ResultType>(
        callback: impl FnOnce(&mut Clipboard) -> Result<ResultType, arboard::Error>,
    ) -> Result<ResultType, arboard::Error> {
        match SHARED_CLIPBOARD.get_or_init(|| Clipboard::new().ok().map(Mutex::new)) {
            Some(clipboard_mutex) => callback(&mut clipboard_mutex.lock()),
            None => Err(arboard::Error::ClipboardNotSupported),
        }
    }

    pub(super) fn clear() {
        if with_shared_clipboard(|clipboard| clipboard.clear()).is_err() {
            fallback_clipboard::clear();
        }
    }

    pub(super) fn get_text(request: StringRequest) {
        if let Ok(text) = with_shared_clipboard(|clipboard| clipboard.get_text()) {
            request.success(text);
            return;
        };
        fallback_clipboard::get_text(request);
    }

    pub(super) fn set_text(new_contents: String) {
        if with_shared_clipboard(|clipboard| clipboard.set_text(&new_contents)).is_err() {
            fallback_clipboard::set_text(new_contents);
        }
    }
}

#[cfg(any(not(feature = "clipboard"), target_os = "android", target_env = "ohos"))]
mod clipboard {
    use super::StringRequest;
    use crate::clipboard_delegate::fallback_clipboard;

    pub(super) fn clear() {
        fallback_clipboard::clear();
    }

    pub(super) fn get_text(request: StringRequest) {
        fallback_clipboard::get_text(request);
    }

    pub(super) fn set_text(new_contents: String) {
        fallback_clipboard::set_text(new_contents);
    }
}