1use alloc::{
10 rc::Rc,
11 string::{String, ToString},
12};
13use core::error::Error;
14use core::fmt;
15
16#[cfg(target_arch = "wasm32")]
17use executor_core::spawn_local;
18use nami::SignalExt;
19use suiteki::Str;
20use waterui_core::gesture::{GestureObserver, TapGesture};
21use waterui_core::resolve::Resolvable;
22use waterui_core::view::{ConfigurableView, Hook, ViewConfiguration};
23use waterui_core::{AnyView, Environment, Metadata, View};
24use waterui_graphics::color::{
25 AccentColor, Color, CurrentColorScheme, MutedForegroundColor, SurfaceVariantColor,
26};
27use waterui_layout::{
28 background::background,
29 padding::{EdgeInsets, Padding},
30 spacer,
31 stack::{HorizontalAlignment, VStack, hstack},
32};
33
34use crate::{
35 font::{Body, Font},
36 highlight::{DefaultHighlighter, Language, highlight_text},
37 text,
38};
39
40#[cfg(all(
42 not(target_os = "android"),
43 not(target_arch = "wasm32"),
44 not(target_os = "espidf")
45))]
46fn copy_to_clipboard(text: &str) {
47 match arboard::Clipboard::new() {
48 Ok(mut clipboard) => {
49 if let Err(error) = clipboard.set_text(text) {
50 tracing::error!(%error, "Failed to copy to clipboard");
51 }
52 }
53 Err(error) => {
54 tracing::error!(%error, "Failed to access clipboard");
55 }
56 }
57}
58
59#[cfg(target_os = "espidf")]
61fn copy_to_clipboard(_text: &str) {}
62
63#[cfg(target_os = "android")]
65fn copy_to_clipboard(text: &str) {
66 if let Err(error) = android_clipboard::set_text(text.to_string()) {
67 tracing::error!(%error, "Failed to copy to clipboard");
68 }
69}
70
71#[cfg(target_arch = "wasm32")]
73fn copy_to_clipboard(text: &str) {
74 let clipboard = web_sys::window()
75 .expect("browser window is unavailable for clipboard access")
76 .navigator()
77 .clipboard();
78 let text = text.to_string();
79
80 spawn_local(async move {
81 let promise = clipboard.write_text(&text);
82 if let Err(error) = wasm_bindgen_futures::JsFuture::from(promise).await {
83 tracing::error!(?error, "Failed to write browser clipboard text");
84 }
85 })
86 .detach();
87}
88
89#[derive(Clone)]
96pub struct OnCopied(Rc<dyn Fn(&Environment)>);
97
98impl OnCopied {
99 pub fn call(&self, env: &Environment) {
101 (self.0)(env);
102 }
103}
104
105impl fmt::Debug for OnCopied {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.write_str("OnCopied(..)")
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct Code {
114 info: Option<Str>,
115 language: Language,
116 content: Str,
117 on_copied: Option<OnCopied>,
118}
119
120impl Code {
121 pub fn new(language: impl TryInto<Language, Error: Error>, content: impl Into<Str>) -> Self {
127 Self {
128 info: None,
129 language: language.try_into().expect("Invalid language"),
130 content: content.into(),
131 on_copied: None,
132 }
133 }
134
135 #[must_use]
141 pub fn info(mut self, info: impl Into<Str>) -> Self {
142 self.info = Some(info.into());
143 self
144 }
145
146 #[must_use]
151 pub fn on_copied(mut self, callback: impl Fn(&Environment) + 'static) -> Self {
152 self.on_copied = Some(OnCopied(Rc::new(callback)));
153 self
154 }
155}
156
157#[derive(Debug, Clone)]
163pub struct CodeConfig {
164 pub info: Option<Str>,
166 pub language: Language,
169 pub content: Str,
171 pub on_copied: Option<OnCopied>,
174}
175
176impl ViewConfiguration for CodeConfig {
177 type View = Code;
178
179 fn render(self) -> Self::View {
180 Code {
181 info: self.info,
182 language: self.language,
183 content: self.content,
184 on_copied: self.on_copied,
185 }
186 }
187}
188
189impl ConfigurableView for Code {
190 type Config = CodeConfig;
191
192 fn config(self) -> Self::Config {
193 CodeConfig {
194 info: self.info,
195 language: self.language,
196 content: self.content,
197 on_copied: self.on_copied,
198 }
199 }
200}
201
202impl View for Code {
203 fn body(self, env: &Environment) -> impl View {
204 let config = self.config();
205 if let Some(hook) = env.get::<Hook<CodeConfig>>() {
210 AnyView::new(hook.apply(env, config))
211 } else {
212 AnyView::new(default_rendering(env, config))
213 }
214 }
215}
216
217fn default_rendering(env: &Environment, config: CodeConfig) -> impl View {
226 let CodeConfig {
227 info: _,
228 language,
229 content,
230 on_copied,
231 } = config;
232 let lang_name = language.to_string();
233 let content_for_copy = content.to_string();
234
235 let highlighted = CurrentColorScheme
236 .resolve(env)
237 .map(move |scheme| {
238 highlight_text(
239 language.clone(),
240 &content,
241 &mut DefaultHighlighter::new(scheme),
242 )
243 })
244 .computed();
245
246 let block = VStack::new(
247 HorizontalAlignment::Leading,
248 8.0,
249 (
250 hstack((
251 text(lang_name)
252 .bold()
253 .color(Color::new(MutedForegroundColor)),
254 spacer(),
255 copy_button(content_for_copy, on_copied),
256 )),
257 text(highlighted).font(Font::from(Body).size(14.0).monospaced()),
258 ),
259 );
260 background(
261 Padding::new(EdgeInsets::all(14.0), block),
262 Color::new(SurfaceVariantColor),
263 )
264}
265
266fn copy_button(content: String, on_copied: Option<OnCopied>) -> impl View {
267 Metadata::new(
268 text("Copy").color(Color::new(AccentColor)),
269 GestureObserver::new(TapGesture::new(), move |env: Environment| {
270 copy_to_clipboard(&content);
271 if let Some(on_copied) = &on_copied {
272 on_copied.call(&env);
273 }
274 }),
275 )
276}
277
278pub fn code(language: impl TryInto<Language, Error: Error>, content: impl Into<Str>) -> Code {
280 Code::new(language, content)
281}