use alloc::{
rc::Rc,
string::{String, ToString},
};
use core::error::Error;
use core::fmt;
#[cfg(target_arch = "wasm32")]
use executor_core::spawn_local;
use nami::SignalExt;
use waterui_core::gesture::{GestureObserver, TapGesture};
use waterui_core::resolve::Resolvable;
use waterui_core::view::{ConfigurableView, Hook, ViewConfiguration};
use waterui_core::{AnyView, Environment, Metadata, View};
use waterui_graphics::color::{
AccentColor, Color, CurrentColorScheme, MutedForegroundColor, SurfaceVariantColor,
};
use waterui_layout::{
background::background,
padding::{EdgeInsets, Padding},
spacer,
stack::{HorizontalAlignment, VStack, hstack},
};
use waterui_str::Str;
use crate::{
font::{Body, Font},
highlight::{DefaultHighlighter, Language, highlight_text},
text,
};
#[cfg(all(
not(target_os = "android"),
not(target_arch = "wasm32"),
not(target_os = "espidf")
))]
fn copy_to_clipboard(text: &str) {
match arboard::Clipboard::new() {
Ok(mut clipboard) => {
if let Err(error) = clipboard.set_text(text) {
tracing::error!(%error, "Failed to copy to clipboard");
}
}
Err(error) => {
tracing::error!(%error, "Failed to access clipboard");
}
}
}
#[cfg(target_os = "espidf")]
fn copy_to_clipboard(_text: &str) {}
#[cfg(target_os = "android")]
fn copy_to_clipboard(text: &str) {
if let Err(error) = android_clipboard::set_text(text.to_string()) {
tracing::error!(%error, "Failed to copy to clipboard");
}
}
#[cfg(target_arch = "wasm32")]
fn copy_to_clipboard(text: &str) {
let clipboard = web_sys::window()
.expect("browser window is unavailable for clipboard access")
.navigator()
.clipboard();
let text = text.to_string();
spawn_local(async move {
let promise = clipboard.write_text(&text);
if let Err(error) = wasm_bindgen_futures::JsFuture::from(promise).await {
tracing::error!(?error, "Failed to write browser clipboard text");
}
})
.detach();
}
#[derive(Clone)]
pub struct OnCopied(Rc<dyn Fn(&Environment)>);
impl OnCopied {
pub fn call(&self, env: &Environment) {
(self.0)(env);
}
}
impl fmt::Debug for OnCopied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("OnCopied(..)")
}
}
#[derive(Debug, Clone)]
pub struct Code {
info: Option<Str>,
language: Language,
content: Str,
on_copied: Option<OnCopied>,
}
impl Code {
pub fn new(language: impl TryInto<Language, Error: Error>, content: impl Into<Str>) -> Self {
Self {
info: None,
language: language.try_into().expect("Invalid language"),
content: content.into(),
on_copied: None,
}
}
#[must_use]
pub fn info(mut self, info: impl Into<Str>) -> Self {
self.info = Some(info.into());
self
}
#[must_use]
pub fn on_copied(mut self, callback: impl Fn(&Environment) + 'static) -> Self {
self.on_copied = Some(OnCopied(Rc::new(callback)));
self
}
}
#[derive(Debug, Clone)]
pub struct CodeConfig {
pub info: Option<Str>,
pub language: Language,
pub content: Str,
pub on_copied: Option<OnCopied>,
}
impl ViewConfiguration for CodeConfig {
type View = Code;
fn render(self) -> Self::View {
Code {
info: self.info,
language: self.language,
content: self.content,
on_copied: self.on_copied,
}
}
}
impl ConfigurableView for Code {
type Config = CodeConfig;
fn config(self) -> Self::Config {
CodeConfig {
info: self.info,
language: self.language,
content: self.content,
on_copied: self.on_copied,
}
}
}
impl View for Code {
fn body(self, env: &Environment) -> impl View {
let config = self.config();
if let Some(hook) = env.get::<Hook<CodeConfig>>() {
AnyView::new(hook.apply(env, config))
} else {
AnyView::new(default_rendering(env, config))
}
}
}
fn default_rendering(env: &Environment, config: CodeConfig) -> impl View {
let CodeConfig {
info: _,
language,
content,
on_copied,
} = config;
let lang_name = language.to_string();
let content_for_copy = content.to_string();
let highlighted = CurrentColorScheme
.resolve(env)
.map(move |scheme| {
highlight_text(
language.clone(),
&content,
&mut DefaultHighlighter::new(scheme),
)
})
.computed();
let block = VStack::new(
HorizontalAlignment::Leading,
8.0,
(
hstack((
text(lang_name)
.bold()
.color(Color::new(MutedForegroundColor)),
spacer(),
copy_button(content_for_copy, on_copied),
)),
text(highlighted).font(Font::from(Body).size(14.0)),
),
);
background(
Padding::new(EdgeInsets::all(14.0), block),
Color::new(SurfaceVariantColor),
)
}
fn copy_button(content: String, on_copied: Option<OnCopied>) -> impl View {
Metadata::new(
text("Copy").color(Color::new(AccentColor)),
GestureObserver::new(TapGesture::new(), move |env: Environment| {
copy_to_clipboard(&content);
if let Some(on_copied) = &on_copied {
on_copied.call(&env);
}
}),
)
}
pub fn code(language: impl TryInto<Language, Error: Error>, content: impl Into<Str>) -> Code {
Code::new(language, content)
}