ul_next/platform.rs
1//! Platform functions to configure `Ultralight` and provide user-defined
2//! implementations for various platform operations.
3//!
4//! The configurations applied to the platform should be set before creating
5//! a [`Renderer`](crate::renderer::Renderer) instance.
6use std::{
7 path::Path,
8 sync::{Arc, Mutex},
9};
10
11#[allow(unused_imports)]
12use crate::error::CreationError;
13
14use crate::{
15 gpu_driver::{self, GpuDriver},
16 string::UlString,
17 Library,
18};
19
20// static globals for holding Rust implementations of platform structs,
21// these will be used on callbacks from the C APIs.
22lazy_static::lazy_static! {
23 static ref LOGGER: InternalPlatform<Box<dyn Logger + Send>> = InternalPlatform::new();
24 static ref CLIPBOARD: InternalPlatform<Box<dyn Clipboard + Send>> = InternalPlatform::new();
25 static ref FILESYSTEM: InternalPlatform<Box<dyn FileSystem + Send>> = InternalPlatform::new();
26 static ref FONTLOADER: InternalPlatform<Box<dyn FontLoader + Send>> = InternalPlatform::new();
27 pub(crate) static ref GPUDRIVER: InternalPlatform<Box<dyn GpuDriver + Send>> = InternalPlatform::new();
28}
29
30pub(crate) struct InternalPlatform<T> {
31 pub(crate) lib: Mutex<Option<Arc<Library>>>,
32 pub(crate) obj: Mutex<Option<T>>,
33}
34
35impl<T> InternalPlatform<T> {
36 pub(crate) const fn new() -> Self {
37 Self {
38 lib: Mutex::new(None),
39 obj: Mutex::new(None),
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug)]
45/// Log levels for the logger. (See [`Logger::log_message`])
46pub enum LogLevel {
47 /// Info level
48 Info = ul_sys::ULLogLevel_kLogLevel_Info as isize,
49 /// Warning level
50 Warning = ul_sys::ULLogLevel_kLogLevel_Warning as isize,
51 /// Error level
52 Error = ul_sys::ULLogLevel_kLogLevel_Error as isize,
53}
54
55impl TryFrom<u32> for LogLevel {
56 type Error = ();
57
58 fn try_from(value: u32) -> Result<Self, ()> {
59 match value {
60 ul_sys::ULLogLevel_kLogLevel_Info => Ok(LogLevel::Info),
61 ul_sys::ULLogLevel_kLogLevel_Warning => Ok(LogLevel::Warning),
62 ul_sys::ULLogLevel_kLogLevel_Error => Ok(LogLevel::Error),
63 _ => Err(()),
64 }
65 }
66}
67
68/// This can be used to log debug messages to the console or to a log file.
69///
70/// This is intended to be implemented by users and defined before creating the Renderer.
71///
72/// (See [`platform::set_logger`](set_logger))
73pub trait Logger {
74 /// Invoked when the library wants to print a message to the log.
75 fn log_message(&mut self, log_level: LogLevel, message: String);
76}
77
78/// This is used for reading and writing data to the platform Clipboard.
79///
80/// [`App`](crate::app::App) automatically provides a platform-specific implementation of this that cuts, copies,
81/// and pastes to the OS clipboard.
82///
83/// If you are using [`Renderer::create`](crate::renderer::Renderer::create)
84/// instead of [`App::new`](crate::app::App::new), you will
85/// need to provide your own implementation of this.
86/// (See [`platform::set_clipboard`](set_clipboard))
87pub trait Clipboard {
88 /// Clear the clipboard.
89 fn clear(&mut self);
90
91 /// Read plaintext from the clipboard.
92 ///
93 /// Invoked when the library wants to read from the system's clipboard.
94 fn read_plain_text(&mut self) -> Option<String>;
95
96 /// Write plaintext to the clipboard.
97 ///
98 /// Invoked when the library wants to write to the system's clipboard.
99 fn write_plain_text(&mut self, text: &str);
100}
101
102/// This is used for loading File URLs (eg, <file:///page.html>).
103///
104/// You can provide the library with your own FileSystem implementation so that file assets are
105/// loaded from your own pipeline (useful if you would like to encrypt/compress your file assets or
106/// ship it in a custom format).
107///
108/// AppCore automatically provides a platform-specific implementation of this that loads files from
109/// a local directory when you call [`App::new`]
110///
111/// If you are using [`Renderer::create`] instead, you will need to provide your own implementation
112/// via [`platform::set_filesystem`].
113///
114/// To provide your own custom FileSystem implementation, you should implement
115/// this trait, and then pass an instance of your struct to
116/// [`platform::set_filesystem`] before calling [`Renderer::create`] or [`App::new`].
117///
118/// [`App::new`]: crate::app::App::new
119/// [`Renderer::create`]: crate::renderer::Renderer::create
120/// [`platform::set_filesystem`]: set_filesystem
121pub trait FileSystem {
122 /// Check if file path exists, return true if exists.
123 fn file_exists(&mut self, path: &str) -> bool;
124
125 /// Get the mime-type of the file (eg "text/html").
126 ///
127 /// This is usually determined by analyzing the file extension.
128 ///
129 /// If a mime-type cannot be determined, this should return "application/unknown".
130 fn get_file_mime_type(&mut self, path: &str) -> String;
131
132 /// Get the charset / encoding of the file (eg "utf-8", "iso-8859-1").
133 ///
134 /// This is only applicable for text-based files (eg, "text/html", "text/plain")
135 /// and is usually determined by analyzing the contents of the file.
136 ///
137 /// If a charset cannot be determined, a safe default to return is "utf-8".
138 fn get_file_charset(&mut self, path: &str) -> String;
139
140 /// Open file for reading and map it to a Buffer.
141 ///
142 /// If the file was unable to be opened, you should return `None`.
143 fn open_file(&mut self, path: &str) -> Option<Vec<u8>>;
144}
145
146/// Represents a font file, either on-disk path or in-memory file contents.
147pub struct FontFile {
148 lib: Arc<Library>,
149 internal: ul_sys::ULFontFile,
150}
151
152impl FontFile {
153 // TODO: support buffers
154 // pub fn from_buffer(...) -> Option<Self> {
155 //}
156
157 /// Create a font file from an on-disk file path.
158 ///
159 /// The file path should already exist.
160 pub fn from_path<P: AsRef<Path>>(lib: Arc<Library>, path: P) -> Option<Self> {
161 unsafe {
162 let path = UlString::from_str(lib.clone(), path.as_ref().to_str().unwrap()).unwrap();
163
164 let internal = lib.ultralight().ulFontFileCreateFromFilePath(path.to_ul());
165 if internal.is_null() {
166 return None;
167 }
168 Some(FontFile { lib, internal })
169 }
170 }
171
172 #[allow(dead_code)]
173 pub(crate) fn to_ul(&self) -> ul_sys::ULFontFile {
174 self.internal
175 }
176}
177
178impl Drop for FontFile {
179 fn drop(&mut self) {
180 unsafe {
181 self.lib.ultralight().ulDestroyFontFile(self.internal);
182 }
183 }
184}
185
186/// User-defined font loader interface.
187///
188/// The library uses this to load all system fonts.
189///
190/// Every operating system has its own library of installed system fonts. The FontLoader interface
191/// is used to lookup these fonts and fetch the actual font data (raw TTF/OTF file data) for a given
192/// given font description.
193///
194/// ## Usage
195///
196/// To provide your own custom FontLoader implementation, you should implement this trait, and then
197/// pass an instance of your struct to [`platform::set_fontloader`]. before calling [`Renderer::create`]
198/// or [`App::new`].
199///
200/// ## Note
201///
202/// > There is a bug in Ultralight right now, and we can't use custom Fontloader for now. So you
203/// > can ignore this for now.
204///
205/// AppCore uses a default OS-specific FontLoader implementation when you call [`App::new`].
206///
207/// If you are using [`Renderer::create`], you can still use AppCore's implementation by calling
208/// [`platform::enable_platform_fontloader`][enable_platform_fontloader].
209///
210///
211/// [`App::new`]: crate::app::App::new
212/// [`Renderer::create`]: crate::renderer::Renderer::create
213pub trait FontLoader {
214 /// Fallback font family name. Will be used if all other fonts fail to load.
215 ///
216 /// This font should be guaranteed to exist (eg, ULFontLoader::load should not fail when
217 /// when passed this font family name).
218 ///
219 /// Return a font family name.
220 fn get_fallback_font(&mut self) -> String;
221
222 /// Fallback font family name that can render the specified characters. This is mainly used to
223 /// support CJK (Chinese, Japanese, Korean) text display
224 ///
225 /// # Arguments
226 /// * `characters` - One or more UTF-16 characters. This is almost always a single character
227 /// * `weight` - Font weight
228 /// * `italic` - Whether or not the font should be italic
229 ///
230 /// Should return a font family name that can render the text
231 fn get_fallback_font_for_characters(
232 &mut self,
233 characters: &str,
234 weight: i32,
235 italic: bool,
236 ) -> String;
237
238 /// Get the actual font file data (TTF/OTF) for a given font description.
239 ///
240 /// # Arguments
241 /// * `family` - Font family name
242 /// * `weight` - Font weight
243 /// * `italic` - Whether or not the font should be italic
244 ///
245 /// Should return a [`FontFile`] that contains the font data. You can return `None`
246 /// here and the loader will fallback to another font.
247 fn load(&mut self, family: &str, weight: i32, italic: bool) -> Option<FontFile>;
248}
249
250platform_set_interface_macro! {
251 /// Set a custom Logger implementation.
252 ///
253 /// This is used to log debug messages to the console or to a log file.
254 ///
255 /// You should call this before [`App::new`] or [`Renderer::create`].
256 ///
257 /// [`App::new`] will use the default logger if you never call this.
258 ///
259 /// If you're [`Renderer::create`] you can still use the
260 /// default logger by calling
261 /// [`platform::enable_default_logger`](enable_default_logger).
262 ///
263 /// [`App::new`]: crate::app::App::new
264 /// [`Renderer::create`]: crate::renderer::Renderer::create
265 pub set_logger<Logger>(lib, logger -> LOGGER) -> ulPlatformSetLogger(ULLogger) {
266 // TODO: handle errors
267 log_message((ul_log_level: u32, ul_message: ul_sys::ULString)) -> ((log_level: u32, message: String)) {
268 let log_level = LogLevel::try_from(ul_log_level).unwrap();
269 let message = UlString::copy_raw_to_string(&lib, ul_message).unwrap();
270 }
271 }
272}
273
274platform_set_interface_macro! {
275 /// Set a custom Clipboard implementation.
276 ///
277 /// This should be used if you are using [`Renderer::create`] (which does not provide its own
278 /// clipboard implementation).
279 ///
280 /// The Clipboard trait is used by the library to make calls to the system's native clipboard
281 /// (eg, cut, copy, paste).
282 ///
283 /// You should call this before [`Renderer::create`] or [`App::new`].
284 ///
285 /// [`App::new`]: crate::app::App::new
286 /// [`Renderer::create`]: crate::renderer::Renderer::create
287 pub set_clipboard<Clipboard>(lib, clipboard -> CLIPBOARD) -> ulPlatformSetClipboard(ULClipboard) {
288 // TODO: handle errors
289 clear() -> () {}
290 read_plain_text((ul_result: ul_sys::ULString)) -> (() -> result: Option<String>) {
291 // no need to preprocess since we're returning a string
292 } {
293 if let Some(result) = result {
294 let result = UlString::from_str(lib.clone(), &result).unwrap();
295 lib.ultralight().ulStringAssignString(ul_result, result.to_ul());
296 }
297 }
298 write_plain_text((ul_text: ul_sys::ULString)) -> ((text: &String)) {
299 let text = UlString::copy_raw_to_string(&lib, ul_text).unwrap();
300 let text = &text;
301 }
302 }
303}
304
305platform_set_interface_macro! {
306 /// Set a custom FileSystem implementation.
307 ///
308 /// The library uses this to load all file URLs (eg, <file:///page.html>).
309 ///
310 /// You can provide the library with your own FileSystem implementation
311 /// so that file assets are loaded from your own pipeline.
312 ///
313 /// You should call this before [`Renderer::create`] or [`App::new`].
314 ///
315 /// Note: [`App::new`] will use the default platform file system if you never call this.
316 ///
317 /// If you're not using [`App::new`], (eg, using [`Renderer::create`]) you
318 /// can still use the default platform file system by calling
319 /// [`platform::enable_platform_filesystem`](enable_platform_filesystem).
320 ///
321 /// [`App::new`]: crate::app::App::new
322 /// [`Renderer::create`]: crate::renderer::Renderer::create
323 pub set_filesystem<FileSystem>(lib, filesystem -> FILESYSTEM) -> ulPlatformSetFileSystem(ULFileSystem) {
324 // TODO: handle errors
325 file_exists((ul_path: ul_sys::ULString) -> bool) -> ((path: &str)) {
326 let path = UlString::copy_raw_to_string(&lib, ul_path).unwrap();
327 let path = &path;
328 }
329 get_file_mime_type((ul_path: ul_sys::ULString) -> ul_sys::ULString) -> ((path: &str) -> result: String) {
330 let path = UlString::copy_raw_to_string(&lib, ul_path).unwrap();
331 let path = &path;
332 } {
333 UlString::from_str_unmanaged(&lib, &result).unwrap()
334 }
335 get_file_charset((ul_path: ul_sys::ULString) -> ul_sys::ULString) -> ((path: &str) -> result: String) {
336 let path = UlString::copy_raw_to_string(&lib, ul_path).unwrap();
337 let path = &path;
338 } {
339 UlString::from_str_unmanaged(&lib, &result).unwrap()
340 }
341 open_file((ul_path: ul_sys::ULString) -> ul_sys::ULBuffer) -> ((path: &str) -> result: Option<Vec<u8>>) {
342 let path = UlString::copy_raw_to_string(&lib, ul_path).unwrap();
343 let path = &path;
344 } {
345 if let Some(result) = result {
346 lib.ultralight().ulCreateBufferFromCopy(result.as_ptr() as _, result.len())
347 } else{
348 std::ptr::null_mut()
349 }
350 }
351 }
352}
353
354// TODO: for some reason, `ulPlatformSetFontLoader` is found in the headers, but not yet the binaries
355// platform_set_interface_macro! {
356// /// Set a custom FontLoader implementation.
357// ///
358// /// The library uses this to load all system fonts.
359// ///
360// /// Every operating system has its own library of installed system fonts. The FontLoader interface
361// /// is used to lookup these fonts and fetch the actual font data (raw TTF/OTF file data) for a given
362// /// given font description.
363// ///
364// /// You should call this before [`Renderer::create`] or [`App::new`].
365// ///
366// /// Note: [`App::new`] will use the default platform font loader if you never call this.
367// ///
368// /// [`App::new`]: crate::app::App::new
369// /// [`Renderer::create`]: crate::renderer::Renderer::create
370// pub set_fontloader<FontLoader>(font_loader -> FONTLOADER) -> ulPlatformSetFontLoader(ULFontLoader) {
371// // TODO: handle errors
372// get_fallback_font(() -> ul_sys::ULString) -> (() -> result: String) {
373// // no need to preprocess since we're returning a string
374// } {
375// UlString::from_str_unmanaged(&result).unwrap()
376// }
377
378// get_fallback_font_for_characters(
379// (ul_characters: ul_sys::ULString, weight: i32, italic: bool) -> ul_sys::ULString) ->
380// ((characters: &str, weight: i32, italic: bool) -> result: String)
381// {
382// let characters = UlString::copy_raw_to_string(ul_characters).unwrap();
383// let characters = &characters;
384// } {
385// UlString::from_str_unmanaged(&result).unwrap()
386// }
387
388// load((ul_family: ul_sys::ULString, weight: i32, italic: bool) -> ul_sys::ULFontFile) ->
389// ((family: &str, weight: i32, italic: bool) -> result: Option<FontFile>)
390// {
391// let family = UlString::copy_raw_to_string(ul_family).unwrap();
392// let family = &family;
393// } {
394// if let Some(result) = result {
395// let r = result.to_ul();
396// // Assuming Ultralight will take ownership of this
397// core::mem::forget(result);
398// r
399// } else {
400// std::ptr::null_mut()
401// }
402// }
403// }
404// }
405
406/// Set a custom GPUDriver implementation.
407///
408/// This should be used if you have enabled the GPU renderer in the Config and are using
409/// [`Renderer`](crate::renderer::Renderer) (which does not provide its own GPUDriver implementation).
410///
411/// The GpuDriver trait is used by the library to dispatch GPU calls to your native GPU context
412/// (eg, D3D11, Metal, OpenGL, Vulkan, etc.) There are reference implementations for this interface
413/// in the [`AppCore`](https://github.com/ultralight-ux/AppCore) repo as well
414/// as a custom implementation for `glium` in [`glium`](crate::gpu_driver::glium).
415///
416/// You should call this before [`Renderer::create`](crate::renderer::Renderer::create).
417pub fn set_gpu_driver<G: GpuDriver + Send + 'static>(lib: Arc<Library>, driver: G) {
418 gpu_driver::set_gpu_driver(lib, driver)
419}
420
421/// Initializes the default logger (writes the log to a file).
422///
423/// This is only needed if you are not calling [`App::new`](crate::app::App::new)
424///
425/// You should specify a writable log path to write the log to for example “./ultralight.log”.
426#[cfg(any(feature = "appcore_linked", feature = "loaded"))]
427#[cfg_attr(docsrs, doc(cfg(any(feature = "appcore_linked", feature = "loaded"))))]
428pub fn enable_default_logger<P: AsRef<Path>>(
429 lib: Arc<Library>,
430 log_path: P,
431) -> Result<(), CreationError> {
432 unsafe {
433 // TODO: handle error
434 let log_path = UlString::from_str(lib.clone(), log_path.as_ref().to_str().unwrap())?;
435 lib.appcore().ulEnableDefaultLogger(log_path.to_ul());
436 }
437 Ok(())
438}
439
440/// Initializes the platform font loader and sets it as the current FontLoader.
441///
442/// This is only needed if you are not calling [`App::new`](crate::app::App::new)
443#[cfg(any(feature = "appcore_linked", feature = "loaded"))]
444#[cfg_attr(docsrs, doc(cfg(any(feature = "appcore_linked", feature = "loaded"))))]
445pub fn enable_platform_fontloader(lib: Arc<Library>) {
446 unsafe {
447 lib.appcore().ulEnablePlatformFontLoader();
448 }
449}
450
451/// Initializes the platform file system (needed for loading file:/// URLs) and sets it as the current FileSystem.
452///
453/// This is only needed if you are not calling [`App::new`](crate::app::App::new)
454///
455/// You can specify a base directory path to resolve relative paths against
456#[cfg(any(feature = "appcore_linked", feature = "loaded"))]
457#[cfg_attr(docsrs, doc(cfg(any(feature = "appcore_linked", feature = "loaded"))))]
458pub fn enable_platform_filesystem<P: AsRef<Path>>(
459 lib: Arc<Library>,
460 base_dir: P,
461) -> Result<(), CreationError> {
462 unsafe {
463 // TODO: handle error
464 let base_dir = UlString::from_str(lib.clone(), base_dir.as_ref().to_str().unwrap())?;
465 lib.appcore().ulEnablePlatformFileSystem(base_dir.to_ul());
466 }
467 Ok(())
468}