tether/
lib.rs

1#![deny(missing_docs)]
2
3//! Windows that are web views.
4
5use log::error;
6use std::cell::{Cell, RefCell};
7use std::ffi::{c_void, CStr, CString};
8use std::rc::Rc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::{panic, process};
11
12thread_local! {
13    static MAIN_THREAD: Cell<bool> = Cell::new(false);
14}
15
16static INITIALIZED: AtomicBool = AtomicBool::new(false);
17
18/// An event handler; you probably want to implement one.
19///
20/// - When the webpage calls `window.tether`, the message is passed to `handle`.
21/// - The handler is dropped when the window is closed.
22pub trait Handler: 'static {
23    /// The webpage called `window.tether` with the given string.
24    fn handle(&mut self, window: Window, message: &str) {
25        let _ = (window, message);
26    }
27}
28
29impl<F: FnMut(Window, &str) + 'static> Handler for F {
30    fn handle(&mut self, window: Window, message: &str) {
31        (self)(window, message)
32    }
33}
34
35#[derive(Clone)]
36/// A window, which may or may not be open.
37pub struct Window {
38    data: Rc<RefCell<Option<raw::tether>>>,
39}
40
41type Data = (Window, Box<dyn Handler>);
42
43impl Window {
44    /// Make a new window with the given options.
45    pub fn new(opts: Options) -> Self {
46        assert_main();
47
48        let this = Window {
49            data: Rc::new(RefCell::new(None)),
50        };
51
52        let handler = opts.handler.unwrap_or(Box::new(|_, _: &_| {}));
53
54        let opts = raw::tether_options {
55            initial_width: opts.initial_width,
56            initial_height: opts.initial_height,
57            minimum_width: opts.minimum_width,
58            minimum_height: opts.minimum_height,
59
60            borderless: opts.borderless,
61            debug: opts.debug,
62
63            data: Box::<Data>::into_raw(Box::new((this.clone(), handler))) as _,
64            closed: Some(closed),
65            message: Some(message),
66        };
67
68        let raw = unsafe { raw::tether_new(opts) };
69        this.data.replace(Some(raw));
70
71        unsafe extern fn closed(data: *mut c_void) {
72            abort_on_panic(|| {
73                let _ = Box::<Data>::from_raw(data as _);
74            });
75        }
76
77        unsafe extern fn message(data: *mut c_void, message: *const i8) {
78            abort_on_panic(|| {
79                let data = data as *mut Data;
80
81                match CStr::from_ptr(message).to_str() {
82                    Ok(message) => {
83                        (*data).1.handle((*data).0.clone(), message);
84                    }
85                    Err(e) => {
86                        error!("{}", e);
87                    }
88                }
89            });
90        }
91
92        this
93    }
94
95    /// Make a new window with the default options and the given handler.
96    pub fn with_handler(handler: impl Handler) -> Self {
97        Self::new(Options {
98            handler: Some(Box::new(handler)),
99            ..Default::default()
100        })
101    }
102
103    /// Evaluate the given JavaScript asynchronously.
104    pub fn eval<I: Into<String>>(&self, s: I) {
105        if let Some(data) = *self.data.borrow_mut() {
106            let s = string_to_cstring(s);
107            unsafe {
108                raw::tether_eval(data, s.as_ptr());
109            }
110        }
111    }
112
113    /// Load the given HTML asynchronously.
114    pub fn load<I: Into<String>>(&self, s: I) {
115        if let Some(data) = *self.data.borrow_mut() {
116            let s = string_to_cstring(s);
117            unsafe {
118                raw::tether_load(data, s.as_ptr());
119            }
120        }
121    }
122
123    /// Set this window's title to the given string.
124    pub fn title<I: Into<String>>(&self, s: I) {
125        if let Some(data) = *self.data.borrow_mut() {
126            let s = string_to_cstring(s);
127            unsafe {
128                raw::tether_title(data, s.as_ptr());
129            }
130        }
131    }
132
133    /// Focus this window above the other windows.
134    pub fn focus(&self) {
135        if let Some(data) = *self.data.borrow_mut() {
136            unsafe {
137                raw::tether_focus(data);
138            }
139        }
140    }
141
142    /// Close this window.
143    pub fn close(&self) {
144        if let Some(data) = *self.data.borrow_mut() {
145            unsafe {
146                raw::tether_close(data);
147            }
148        }
149    }
150}
151
152impl Default for Window {
153    fn default() -> Self {
154        Self::new(Default::default())
155    }
156}
157
158/// The window options.
159///
160/// Note that these are mostly *suggestions* rather than *requirements*.
161pub struct Options {
162    /// The initial window width in pixels.
163    pub initial_width: usize,
164    /// The initial window height in pixels.
165    pub initial_height: usize,
166    /// The minimum window width in pixels.
167    pub minimum_width: usize,
168    /// The minimum window height in pixels.
169    pub minimum_height: usize,
170
171    /// Whether to draw the title bar and stuff like that.
172    pub borderless: bool,
173    /// I'm not entirely sure what enabling this does.
174    pub debug: bool,
175
176    /// The window's handler.
177    pub handler: Option<Box<dyn Handler>>,
178}
179
180impl Default for Options {
181    fn default() -> Self {
182        Self {
183            initial_width: 640,
184            initial_height: 480,
185            minimum_width: 480,
186            minimum_height: 360,
187
188            borderless: false,
189            debug: false,
190
191            handler: None,
192        }
193    }
194}
195
196/// Initialize things; call this first.
197///
198/// By calling this function, you're promising us that you haven't called it
199/// before, and that this is the main thread. The provided callback should
200/// contain your "real" main function.
201pub unsafe fn start(cb: fn()) {
202    static mut INIT: Option<fn()> = None;
203    INIT = Some(cb);
204
205    unsafe extern fn init() {
206        abort_on_panic(|| {
207            MAIN_THREAD.with(|initialized| {
208                initialized.set(true);
209            });
210
211            INITIALIZED.store(true, Ordering::Relaxed);
212
213            INIT.unwrap()();
214        });
215    }
216
217    raw::tether_start(Some(init));
218}
219
220/// Terminate the application as gracefully as possible.
221pub fn exit() {
222    assert_main();
223
224    unsafe {
225        raw::tether_exit();
226    }
227}
228
229/// Run the given function on the main thread.
230pub fn dispatch<F: FnOnce() + Send>(f: F) {
231    assert_initialized();
232
233    unsafe {
234        raw::tether_dispatch(
235            Box::<F>::into_raw(Box::new(f)) as _,
236            Some(execute::<F>),
237        );
238    }
239
240    unsafe extern fn execute<F: FnOnce() + Send>(data: *mut c_void) {
241        abort_on_panic(|| {
242            Box::<F>::from_raw(data as _)();
243        });
244    }
245}
246
247fn abort_on_panic<F: FnOnce() + panic::UnwindSafe>(f: F) {
248    if panic::catch_unwind(f).is_err() {
249        process::abort();
250    }
251}
252
253/// Make sure that we're initialized.
254fn assert_initialized() {
255    assert!(INITIALIZED.load(Ordering::Relaxed));
256}
257
258/// Make sure that we're initialized and on the main thread.
259fn assert_main() {
260    MAIN_THREAD.with(|initialized| {
261        assert!(initialized.get());
262    });
263}
264
265fn string_to_cstring<I: Into<String>>(s: I) -> CString {
266    CString::new(s.into()).unwrap()
267}
268
269mod raw {
270    #![allow(dead_code, nonstandard_style)]
271    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
272}