use std::collections::{HashMap, HashSet};
use std::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex, PoisonError};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Runtime};
#[cfg(not(any(target_os = "ios", target_os = "android")))]
use tauri::{PhysicalPosition, PhysicalSize};
use crate::engine::{
self, Audio, Clip, ClipId, Command, Controls, Core, Hold, OutputRect, Planned, Screen, Shared,
};
use crate::error::{Error, Result};
use crate::geometry::Rect;
use crate::placement::{monitor_id, Monitor};
use crate::render::Gpu;
use crate::tauri_native;
type RightClick<R> = Arc<dyn Fn(&str, &tauri::Window<R>) + Send + Sync>;
type Outputs<R> = Arc<Mutex<Vec<(String, tauri::Window<R>)>>>;
pub struct TauriVtome<R: Runtime> {
app: AppHandle<R>,
excluded: HashSet<String>,
frame_rate: f64,
click_through: bool,
cover_menu_bar: bool,
right_click: Option<RightClick<R>>,
windows: Outputs<R>,
controls: Controls,
commands: Mutex<Option<Receiver<Command>>>,
shared: Arc<Shared>,
engine: Mutex<Option<JoinHandle<Option<Error>>>>,
}
#[derive(Clone, Debug)]
pub struct Started {
pub monitors: Vec<Monitor>,
pub running: Vec<String>,
pub skipped: Vec<(String, String)>,
}
impl<R: Runtime> TauriVtome<R> {
pub fn new<I, S>(app: AppHandle<R>, excluded_monitors: I, frame_rate: f64) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let frame_rate = engine::sane_frame_rate(frame_rate);
let (controls, commands, shared) = engine::channel(frame_rate);
TauriVtome {
app,
excluded: excluded_monitors.into_iter().map(Into::into).collect(),
frame_rate,
click_through: true,
cover_menu_bar: false,
right_click: None,
windows: Arc::new(Mutex::new(Vec::new())),
controls,
commands: Mutex::new(Some(commands)),
shared,
engine: Mutex::new(None),
}
}
pub fn click_through(mut self, click_through: bool) -> Self {
self.click_through = click_through;
self
}
pub fn cover_menu_bar(mut self, cover: bool) -> Self {
self.cover_menu_bar = cover;
self
}
pub fn on_right_click(mut self, on_click: impl Fn(&str, &tauri::Window<R>) + Send + Sync + 'static) -> Self {
self.right_click = Some(Arc::new(on_click));
self
}
pub fn window(&self, monitor: &str) -> Option<tauri::Window<R>> {
lock(&self.windows).iter().find(|(id, _)| id == monitor).map(|(_, window)| window.clone())
}
pub fn hide(&self, monitor: &str) -> Result<bool> {
match self.window(monitor) {
Some(window) => window.hide().map(|()| true).map_err(|error| Error::Render {
reason: format!("Tauri would not hide the output on {monitor}: {error}"),
}),
None => Ok(false),
}
}
pub fn hide_all(&self) -> Result<()> {
for (monitor, window) in lock(&self.windows).iter() {
window.hide().map_err(|error| Error::Render {
reason: format!("Tauri would not hide the output on {monitor}: {error}"),
})?;
}
Ok(())
}
pub fn show_all(&self) -> Result<()> {
for (monitor, window) in lock(&self.windows).iter() {
window.show().map_err(|error| Error::Render {
reason: format!("Tauri would not show the output on {monitor}: {error}"),
})?;
}
Ok(())
}
pub fn monitors(&self) -> Result<Vec<Monitor>> {
let tauri_error = |error: tauri::Error| Error::Render {
reason: format!("Tauri would not list the monitors: {error}"),
};
let primary = self.app.primary_monitor().map_err(tauri_error)?;
let monitors = self
.app
.available_monitors()
.map_err(tauri_error)?
.into_iter()
.map(|monitor| {
let name = monitor.name().cloned().unwrap_or_default();
let position = monitor.position();
let size = monitor.size();
let scale_factor = monitor.scale_factor();
let is_primary = primary.as_ref().is_some_and(|primary| {
primary.position() == position && primary.name() == monitor.name()
});
Monitor {
persistent_id: monitor_id(&name, position.x, position.y, scale_factor),
name,
bounds: Rect::new(
f64::from(position.x),
f64::from(position.y),
f64::from(size.width),
f64::from(size.height),
),
scale_factor,
refresh_millihertz: None,
is_primary,
}
})
.collect();
Ok(monitors)
}
pub fn controls(&self) -> Controls {
self.controls.clone()
}
pub fn add(&self, clip: Clip) -> Result<ClipId> {
self.controls.add(clip)
}
pub fn add_image(
&self,
path: impl Into<std::path::PathBuf>,
monitor: impl Into<String>,
hold: Hold,
) -> Result<ClipId> {
self.controls.add_image(path, monitor, hold)
}
pub fn add_frame(
&self,
frame: crate::frame::Frame,
monitor: impl Into<String>,
hold: Hold,
) -> Result<ClipId> {
self.controls.add_frame(frame, monitor, hold)
}
pub fn add_generic(
&self,
path: impl Into<std::path::PathBuf>,
monitor: impl Into<String>,
) -> Result<ClipId> {
self.controls.add_generic(path, monitor)
}
pub fn add_cached(
&self,
clip: Clip,
ranges: Vec<(u64, u64)>,
frames: Vec<crate::frame::Frame>,
) -> Result<ClipId> {
self.controls.add_cached(clip, ranges, frames)
}
pub fn stop(&self, id: ClipId) -> bool {
self.controls.stop(id)
}
pub fn is_running(&self, id: ClipId) -> bool {
self.controls.is_running(id)
}
pub fn is_finished(&self, id: ClipId) -> bool {
self.controls.is_finished(id)
}
pub fn snapshot(&self, id: ClipId) -> Result<crate::frame::Frame> {
self.controls.snapshot(id)
}
pub fn start(
&mut self,
outputs: HashMap<String, OutputRect>,
backgrounds: HashMap<String, String>,
audio: Audio,
) -> Result<Started> {
let commands = self
.commands
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
.ok_or_else(|| Error::unsupported("this TauriVtome has already started"))?;
let attached = self.monitors()?;
let plans = engine::plan(&outputs, &backgrounds, &self.excluded, &attached, &self.shared)?;
let (gpu, screens) = self.open(&plans)?;
let mut core = Core::new(
gpu,
&plans,
screens,
commands,
Arc::clone(&self.shared),
&audio,
)?;
let period = Duration::from_secs_f64(1.0 / self.frame_rate);
let controls = self.controls.clone();
let engine = std::thread::Builder::new()
.name("vtome".to_string())
.spawn(move || {
let failure = run(&mut core, period).err();
for window in core.into_screens().into_iter().map(Screen::into_window) {
let _ = window.destroy();
}
controls.close();
failure
})
.map_err(|error| Error::Render {
reason: format!("the engine thread would not start: {error}"),
})?;
*self.engine.lock().unwrap_or_else(PoisonError::into_inner) = Some(engine);
Ok(Started {
monitors: attached,
running: self.controls.running_monitors(),
skipped: self.controls.skipped_monitors(),
})
}
pub fn close(&self) -> Result<()> {
self.controls.close();
for (monitor, _) in lock(&self.windows).drain(..) {
tauri_native::forget(&label(&monitor));
}
let engine = self.engine.lock().unwrap_or_else(PoisonError::into_inner).take();
match engine.map(JoinHandle::join) {
Some(Ok(Some(error))) => Err(error),
Some(Err(_)) => Err(Error::Render {
reason: "the engine thread panicked".to_string(),
}),
_ => Ok(()),
}
}
fn open(&self, plans: &[Planned]) -> Result<(Gpu, Vec<Screen<tauri::Window<R>>>)> {
let app = self.app.clone();
let options = WindowOptions {
click_through: self.click_through,
cover_menu_bar: self.cover_menu_bar,
right_click: self.right_click.clone(),
};
let plans: Vec<Planned> = plans.to_vec();
let windows = Arc::clone(&self.windows);
let job = move || open_on_main_thread(&app, &plans, &options, &windows);
if is_main_thread() {
return job();
}
let (reply, answer) = std::sync::mpsc::channel();
self.app
.run_on_main_thread(move || {
let _ = reply.send(job());
})
.map_err(|error| Error::Render {
reason: format!("Tauri would not run on its main thread: {error}"),
})?;
answer.recv_timeout(Duration::from_secs(10)).map_err(|_| Error::Render {
reason: "Tauri's main thread did not open the windows within ten seconds — is its \
event loop running?"
.to_string(),
})?
}
}
impl<R: Runtime> Drop for TauriVtome<R> {
fn drop(&mut self) {
let _ = self.close();
}
}
fn run<W>(core: &mut Core<W>, period: Duration) -> Result<()> {
let mut next = Instant::now();
while core.refresh()? {
next += period;
let now = Instant::now();
match next.checked_duration_since(now) {
Some(wait) => std::thread::sleep(wait),
None => next = now,
}
}
Ok(())
}
fn is_main_thread() -> bool {
#[cfg(target_vendor = "apple")]
{
extern "C" {
fn pthread_main_np() -> std::ffi::c_int;
}
unsafe { pthread_main_np() == 1 }
}
#[cfg(not(target_vendor = "apple"))]
{
std::thread::current().name() == Some("main")
}
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn label(monitor: &str) -> String {
format!("vtome-{monitor}")
}
#[cfg_attr(any(target_os = "ios", target_os = "android"), allow(dead_code))]
struct WindowOptions<R: Runtime> {
click_through: bool,
cover_menu_bar: bool,
right_click: Option<RightClick<R>>,
}
fn open_on_main_thread<R: Runtime>(
app: &AppHandle<R>,
plans: &[Planned],
options: &WindowOptions<R>,
opened: &Mutex<Vec<(String, tauri::Window<R>)>>,
) -> Result<(Gpu, Vec<Screen<tauri::Window<R>>>)> {
let windows = plans
.iter()
.map(|plan| window(app, plan, options))
.collect::<Result<Vec<_>>>()?;
*lock(opened) = plans.iter().map(|plan| plan.monitor.clone()).zip(windows.iter().cloned()).collect();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle(
Box::new(windows[0].clone()),
));
let surfaces = windows
.iter()
.map(|window| {
instance
.create_surface(window.clone())
.map_err(|error| Error::Render {
reason: format!("no surface for an output window: {error}"),
})
})
.collect::<Result<Vec<_>>>()?;
let gpu = Gpu::from_instance(instance, surfaces.first())?;
let screens = plans
.iter()
.zip(windows)
.zip(surfaces)
.map(|((plan, window), surface)| Screen::new(plan, window, surface, &gpu))
.collect();
Ok((gpu, screens))
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
fn window<R: Runtime>(
app: &AppHandle<R>,
plan: &Planned,
options: &WindowOptions<R>,
) -> Result<tauri::Window<R>> {
let tauri_error = |error: tauri::Error| Error::Render {
reason: format!("Tauri would not open the window for {}: {error}", plan.monitor),
};
let label = label(&plan.monitor);
let builder = tauri::WindowBuilder::new(app, &label)
.title(format!("vtome — {}", plan.monitor))
.decorations(false)
.resizable(false)
.always_on_top(true)
.focused(false)
.skip_taskbar(true)
.shadow(false)
.visible(false);
#[cfg(not(target_os = "macos"))]
let builder = builder.transparent(true);
let window = builder.build().map_err(tauri_error)?;
window
.set_position(PhysicalPosition::new(plan.x, plan.y))
.map_err(tauri_error)?;
window
.set_size(PhysicalSize::new(plan.width, plan.height))
.map_err(tauri_error)?;
#[cfg(target_os = "macos")]
see_through(&window)?;
if options.cover_menu_bar {
tauri_native::cover_menu_bar(&window)?;
}
if options.click_through {
window.set_ignore_cursor_events(true).map_err(tauri_error)?;
}
if let Some(on_click) = &options.right_click {
let (on_click, monitor, clicked) = (Arc::clone(on_click), plan.monitor.clone(), window.clone());
tauri_native::watch_right_clicks(&window, &label, Arc::new(move || on_click(&monitor, &clicked)))?;
}
window.show().map_err(tauri_error)?;
Ok(window)
}
#[cfg(any(target_os = "ios", target_os = "android"))]
fn window<R: Runtime>(
_app: &AppHandle<R>,
plan: &Planned,
_options: &WindowOptions<R>,
) -> Result<tauri::Window<R>> {
Err(Error::unsupported(format!(
"vtome's output windows are desktop only, so nothing can be opened on {} on iOS or \
Android; external displays there are not built yet (planning/TODO.md §11)",
plan.monitor
)))
}
#[cfg(target_os = "macos")]
fn see_through<R: Runtime>(window: &tauri::Window<R>) -> Result<()> {
use std::ffi::c_void;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
type Id = *mut c_void;
type Sel = *mut c_void;
#[link(name = "objc")]
extern "C" {
fn objc_msgSend();
fn objc_getClass(name: *const std::ffi::c_char) -> Id;
fn sel_registerName(name: *const std::ffi::c_char) -> Sel;
}
let handle = window.window_handle().map_err(|error| Error::Render {
reason: format!("no native handle for an output window: {error}"),
})?;
let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
return Err(Error::Render {
reason: "a macOS window without an AppKit handle".to_string(),
});
};
let send_id: unsafe extern "C" fn(Id, Sel) -> Id =
unsafe { std::mem::transmute(objc_msgSend as unsafe extern "C" fn()) };
let send_bool: unsafe extern "C" fn(Id, Sel, bool) =
unsafe { std::mem::transmute(objc_msgSend as unsafe extern "C" fn()) };
let send_object: unsafe extern "C" fn(Id, Sel, Id) =
unsafe { std::mem::transmute(objc_msgSend as unsafe extern "C" fn()) };
unsafe {
let view = appkit.ns_view.as_ptr();
let ns_window = send_id(view, sel_registerName(c"window".as_ptr()));
if ns_window.is_null() {
return Err(Error::Render {
reason: "an output view that is in no window".to_string(),
});
}
let clear = send_id(
objc_getClass(c"NSColor".as_ptr()),
sel_registerName(c"clearColor".as_ptr()),
);
send_bool(ns_window, sel_registerName(c"setOpaque:".as_ptr()), false);
send_object(ns_window, sel_registerName(c"setBackgroundColor:".as_ptr()), clear);
}
Ok(())
}