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}