use std::{
collections::HashMap,
fmt,
path::PathBuf,
sync::{self, Arc},
};
pub mod raw_device_events;
pub mod raw_events;
use crate::{
event::{event, event_args},
window::{MonitorId, WindowId},
};
use parking_lot::{MappedRwLockReadGuard, MappedRwLockWriteGuard};
use zng_app_context::app_local;
use zng_layout::unit::{DipPoint, DipRect, DipSideOffsets, DipSize, Factor, Frequency, Px, PxPoint, PxRect};
use zng_task::channel::{self, ChannelError, IpcBytes, IpcReceiver, Receiver};
use zng_txt::Txt;
use zng_unique_id::IdMap;
use zng_var::{ArcEq, ResponderVar, Var, VarHandle, WeakEq};
use zng_view_api::{
self, DeviceEventsFilter, DragDropId, Event, FocusResult, ViewProcessGen, ViewProcessInfo,
api_extension::{ApiExtensionId, ApiExtensionName, ApiExtensionPayload, ApiExtensionRecvError},
audio::{
AudioDecoded, AudioId, AudioMetadata, AudioMix, AudioOutputConfig, AudioOutputId as ApiAudioOutputId, AudioOutputOpenData,
AudioOutputRequest, AudioOutputUpdateRequest, AudioPlayId, AudioPlayRequest, AudioRequest,
},
dialog::{FileDialog, FileDialogResponse, MsgDialog, MsgDialogResponse, Notification, NotificationResponse},
drag_drop::{DragDropData, DragDropEffect, DragDropError},
font::{FontOptions, IpcFontBytes},
image::{ImageDecoded, ImageEncodeId, ImageEncodeRequest, ImageMaskMode, ImageMetadata, ImageRequest, ImageTextureId},
window::{
CursorIcon, FocusIndicator, FrameRequest, FrameUpdateRequest, HeadlessOpenData, HeadlessRequest, RenderMode, ResizeDirection,
VideoMode, WindowButton, WindowRequest, WindowStateAll,
},
};
pub(crate) use zng_view_api::{
Controller, raw_input::InputDeviceId as ApiDeviceId, window::MonitorId as ApiMonitorId, window::WindowId as ApiWindowId,
};
use zng_view_api::{
clipboard::{ClipboardData, ClipboardError, ClipboardType},
font::{FontFaceId, FontId, FontVariationName},
image::ImageId,
};
use self::raw_device_events::InputDeviceId;
use super::{APP, AppId};
#[expect(non_camel_case_types)]
pub struct VIEW_PROCESS;
struct ViewProcessService {
process: zng_view_api::Controller,
input_device_ids: HashMap<ApiDeviceId, InputDeviceId>,
monitor_ids: HashMap<ApiMonitorId, MonitorId>,
data_generation: ViewProcessGen,
loading_images: Vec<WeakEq<ViewImageHandleData>>,
encoding_images: Vec<EncodeRequest>,
loading_audios: Vec<WeakEq<ViewAudioHandleData>>,
pending_frames: IdMap<WindowId, usize>,
message_dialogs: Vec<(zng_view_api::dialog::DialogId, ResponderVar<MsgDialogResponse>)>,
file_dialogs: Vec<(zng_view_api::dialog::DialogId, ResponderVar<FileDialogResponse>)>,
notifications: Vec<(zng_view_api::dialog::DialogId, VarHandle, ResponderVar<NotificationResponse>)>,
ping_count: u16,
}
app_local! {
static VIEW_PROCESS_SV: Option<ViewProcessService> = None;
static VIEW_PROCESS_INFO: ViewProcessInfo = const { ViewProcessInfo::new(ViewProcessGen::INVALID, false) };
}
impl VIEW_PROCESS {
pub fn is_available(&self) -> bool {
APP.is_running() && VIEW_PROCESS_SV.read().is_some()
}
fn read(&self) -> MappedRwLockReadGuard<'_, ViewProcessService> {
VIEW_PROCESS_SV.read_map(|e| e.as_ref().expect("VIEW_PROCESS not available"))
}
fn write(&self) -> MappedRwLockWriteGuard<'_, ViewProcessService> {
VIEW_PROCESS_SV.write_map(|e| e.as_mut().expect("VIEW_PROCESS not available"))
}
fn try_write(&self) -> Result<MappedRwLockWriteGuard<'_, ViewProcessService>> {
let vp = VIEW_PROCESS_SV.write();
if let Some(w) = &*vp
&& w.process.is_connected()
{
return Ok(MappedRwLockWriteGuard::map(vp, |w| w.as_mut().unwrap()));
}
Err(ChannelError::disconnected())
}
fn check_app(&self, id: AppId) {
let actual = APP.id();
if Some(id) != actual {
panic!("cannot use view handle from app `{id:?}` in app `{actual:?}`");
}
}
fn handle_write(&self, id: AppId) -> MappedRwLockWriteGuard<'_, ViewProcessService> {
self.check_app(id);
self.write()
}
pub fn is_connected(&self) -> bool {
self.read().process.is_connected()
}
pub fn is_headless_with_render(&self) -> bool {
self.read().process.headless()
}
pub fn is_same_process(&self) -> bool {
self.read().process.same_process()
}
pub fn info(&self) -> MappedRwLockReadGuard<'static, ViewProcessInfo> {
VIEW_PROCESS_INFO.read()
}
pub fn generation(&self) -> ViewProcessGen {
self.read().process.generation()
}
pub fn set_device_events_filter(&self, filter: DeviceEventsFilter) -> Result<()> {
self.write().process.set_device_events_filter(filter)
}
pub fn open_window(&self, config: WindowRequest) -> Result<()> {
self.write().process.open_window(config)
}
pub fn open_headless(&self, config: HeadlessRequest) -> Result<()> {
self.write().process.open_headless(config)
}
pub fn open_audio_output(&self, request: AudioOutputRequest) -> Result<()> {
self.write().process.open_audio_output(request)
}
pub fn add_image(&self, request: ImageRequest<IpcBytes>) -> Result<ViewImageHandle> {
let mut app = self.write();
let id = app.process.add_image(request)?;
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), id));
app.loading_images.push(ArcEq::downgrade(&handle));
Ok(ViewImageHandle(Some(handle)))
}
pub fn add_image_pro(&self, request: ImageRequest<IpcReceiver<IpcBytes>>) -> Result<ViewImageHandle> {
let mut app = self.write();
let id = app.process.add_image_pro(request)?;
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), id));
app.loading_images.push(ArcEq::downgrade(&handle));
Ok(ViewImageHandle(Some(handle)))
}
pub fn encode_image(&self, request: ImageEncodeRequest) -> Receiver<std::result::Result<IpcBytes, EncodeError>> {
let (sender, receiver) = channel::bounded(1);
if request.id != ImageId::INVALID {
let mut app = VIEW_PROCESS.write();
match app.process.encode_image(request) {
Ok(r) => {
app.encoding_images.push(EncodeRequest {
task_id: r,
listener: sender,
});
}
Err(_) => {
let _ = sender.send_blocking(Err(EncodeError::Disconnected));
}
}
} else {
let _ = sender.send_blocking(Err(EncodeError::Dummy));
}
receiver
}
pub fn add_audio(&self, request: AudioRequest<IpcBytes>) -> Result<ViewAudioHandle> {
let mut app = self.write();
let id = app.process.add_audio(request)?;
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), id));
app.loading_audios.push(ArcEq::downgrade(&handle));
Ok(ViewAudioHandle(Some(handle)))
}
pub fn add_audio_pro(&self, request: AudioRequest<IpcReceiver<IpcBytes>>) -> Result<ViewAudioHandle> {
let mut app = self.write();
let id = app.process.add_audio_pro(request)?;
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), id));
app.loading_audios.push(ArcEq::downgrade(&handle));
Ok(ViewAudioHandle(Some(handle)))
}
pub fn clipboard(&self) -> Result<&ViewClipboard> {
if VIEW_PROCESS.is_connected() {
Ok(&ViewClipboard {})
} else {
Err(ChannelError::disconnected())
}
}
pub fn notification_dialog(&self, notification: Var<Notification>, responder: ResponderVar<NotificationResponse>) -> Result<()> {
let mut app = self.write();
let dlg_id = app.process.notification_dialog(notification.get())?;
let handle = notification.hook(move |n| {
let mut app = VIEW_PROCESS.write();
let retain = app.notifications.iter().any(|(id, _, _)| *id == dlg_id);
if retain {
app.process.update_notification(dlg_id, n.value().clone()).ok();
}
retain
});
app.notifications.push((dlg_id, handle, responder));
Ok(())
}
#[deprecated = "use `is_busy`"]
pub fn pending_frames(&self) -> usize {
self.read().pending_frames.values().copied().sum()
}
pub fn is_busy(&self) -> bool {
self.read().pending_frames.values().copied().max().unwrap_or(0) > 1
}
pub fn respawn(&self) {
self.write().process.respawn()
}
pub fn extension_id(&self, extension_name: impl Into<ApiExtensionName>) -> Result<Option<ApiExtensionId>> {
let me = self.read();
if me.process.is_connected() {
Ok(self.info().extensions.id(&extension_name.into()))
} else {
Err(ChannelError::disconnected())
}
}
pub fn third_party_licenses(&self) -> Result<Vec<crate::third_party::LicenseUsed>> {
self.write().process.third_party_licenses()
}
pub fn app_extension_raw(&self, extension_id: ApiExtensionId, extension_request: ApiExtensionPayload) -> Result<ApiExtensionPayload> {
self.write().process.app_extension(extension_id, extension_request)
}
pub fn app_extension<I, O>(&self, extension_id: ApiExtensionId, request: &I) -> Result<std::result::Result<O, ApiExtensionRecvError>>
where
I: serde::Serialize,
O: serde::de::DeserializeOwned,
{
let payload = ApiExtensionPayload::serialize(&request).unwrap();
let response = self.write().process.app_extension(extension_id, payload)?;
Ok(response.deserialize::<O>())
}
pub fn handle_disconnect(&self, vp_gen: ViewProcessGen) {
self.write().process.handle_disconnect(vp_gen)
}
pub(super) fn start<F>(&self, view_process_exe: PathBuf, view_process_env: HashMap<Txt, Txt>, headless: bool, on_event: F)
where
F: FnMut(Event) + Send + 'static,
{
let _s = tracing::debug_span!("VIEW_PROCESS.start", ?view_process_exe, ?view_process_env, ?headless).entered();
let process = zng_view_api::Controller::start(view_process_exe, view_process_env, headless, on_event);
*VIEW_PROCESS_SV.write() = Some(ViewProcessService {
data_generation: process.generation(),
process,
input_device_ids: HashMap::default(),
monitor_ids: HashMap::default(),
loading_images: vec![],
encoding_images: vec![],
loading_audios: vec![],
pending_frames: IdMap::new(),
message_dialogs: vec![],
file_dialogs: vec![],
notifications: vec![],
ping_count: 0,
});
}
pub(crate) fn on_window_opened(&self, window_id: WindowId, data: zng_view_api::window::WindowOpenData) -> (ViewWindow, WindowOpenData) {
let mut app = self.write();
let _ = app.check_generation();
let win = ViewWindow(ArcEq::new(ViewWindowData {
app_id: APP.id().unwrap(),
id: ApiWindowId::from_raw(window_id.get()),
generation: app.data_generation,
}));
drop(app);
let data = WindowOpenData::new(data, |id| self.monitor_id(id));
(win, data)
}
pub(crate) fn on_audio_output_opened(&self, output_id: AudioOutputId, data: AudioOutputOpenData) -> ViewAudioOutput {
let mut app = self.write();
let _ = app.check_generation();
ViewAudioOutput(ArcEq::new(ViewAudioOutputData {
app_id: APP.id().unwrap(),
id: ApiAudioOutputId::from_raw(output_id.get()),
generation: app.data_generation,
data,
}))
}
pub(super) fn input_device_id(&self, id: ApiDeviceId) -> InputDeviceId {
*self.write().input_device_ids.entry(id).or_insert_with(InputDeviceId::new_unique)
}
pub(super) fn monitor_id(&self, id: ApiMonitorId) -> MonitorId {
*self.write().monitor_ids.entry(id).or_insert_with(MonitorId::new_unique)
}
pub(super) fn handle_inited(&self, inited: &zng_view_api::ViewProcessInfo) {
let mut me = self.write();
*VIEW_PROCESS_INFO.write() = inited.clone();
me.process.handle_inited(inited.generation);
}
pub(super) fn handle_suspended(&self) {
self.write().process.handle_suspended();
}
pub(crate) fn on_headless_opened(
&self,
id: WindowId,
data: zng_view_api::window::HeadlessOpenData,
) -> (ViewHeadless, HeadlessOpenData) {
let mut app = self.write();
let _ = app.check_generation();
let surf = ViewHeadless(ArcEq::new(ViewWindowData {
app_id: APP.id().unwrap(),
id: ApiWindowId::from_raw(id.get()),
generation: app.data_generation,
}));
(surf, data)
}
pub(super) fn on_image_metadata(&self, meta: &ImageMetadata) -> Option<ViewImageHandle> {
let mut app = self.write();
let mut found = None;
app.loading_images.retain(|i| {
if let Some(h) = i.upgrade() {
if found.is_none() && h.2 == meta.id {
found = Some(h);
}
true
} else {
false
}
});
if found.is_none() && meta.parent.is_some() {
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), meta.id));
app.loading_images.push(ArcEq::downgrade(&handle));
return Some(ViewImageHandle(Some(handle)));
}
found.map(|h| ViewImageHandle(Some(h)))
}
pub(super) fn on_image_decoded(&self, data: &ImageDecoded) -> Option<ViewImageHandle> {
let mut app = self.write();
let mut found = None;
app.loading_images.retain(|i| {
if let Some(h) = i.upgrade() {
if found.is_none() && h.2 == data.meta.id {
found = Some(h);
return data.partial.is_some();
}
true
} else {
false
}
});
found.map(|h| ViewImageHandle(Some(h)))
}
pub(super) fn on_image_error(&self, id: ImageId) -> Option<ViewImageHandle> {
let mut app = self.write();
let mut found = None;
app.loading_images.retain(|i| {
if let Some(h) = i.upgrade() {
if found.is_none() && h.2 == id {
found = Some(h);
return false;
}
true
} else {
false
}
});
found.map(|h| ViewImageHandle(Some(h)))
}
pub(super) fn on_audio_metadata(&self, meta: &AudioMetadata) -> Option<ViewAudioHandle> {
let mut app = self.write();
let mut found = None;
app.loading_audios.retain(|i| {
if let Some(h) = i.upgrade() {
if found.is_none() && h.2 == meta.id {
found = Some(h);
}
true
} else {
false
}
});
if found.is_none() && meta.parent.is_some() {
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), meta.id));
app.loading_audios.push(ArcEq::downgrade(&handle));
return Some(ViewAudioHandle(Some(handle)));
}
found.map(|h| ViewAudioHandle(Some(h)))
}
pub(super) fn on_audio_decoded(&self, audio: &AudioDecoded) -> Option<ViewAudioHandle> {
let mut app = self.write();
let mut found = None;
app.loading_audios.retain(|i| {
if let Some(h) = i.upgrade() {
if found.is_none() && h.2 == audio.id {
found = Some(h);
return !audio.is_full;
}
true
} else {
false
}
});
found.map(|h| ViewAudioHandle(Some(h)))
}
pub(super) fn on_audio_error(&self, id: AudioId) -> Option<ViewAudioHandle> {
let mut app = self.write();
let mut found = None;
app.loading_audios.retain(|i| {
if let Some(h) = i.upgrade() {
if found.is_none() && h.2 == id {
found = Some(h);
return false;
}
true
} else {
false
}
});
found.map(|h| ViewAudioHandle(Some(h)))
}
pub(crate) fn on_frame_rendered(&self, id: WindowId) {
let mut vp = self.write();
if let Some(c) = vp.pending_frames.get_mut(&id) {
*c = c.saturating_sub(1);
}
}
pub(crate) fn on_frame_image(&self, data: &ImageDecoded) -> ViewImageHandle {
ViewImageHandle(Some(ArcEq::new((APP.id().unwrap(), self.generation(), data.meta.id))))
}
pub(super) fn on_image_encoded(&self, task_id: ImageEncodeId, data: IpcBytes) {
self.on_image_encode_result(task_id, Ok(data));
}
pub(super) fn on_image_encode_error(&self, task_id: ImageEncodeId, error: Txt) {
self.on_image_encode_result(task_id, Err(EncodeError::Encode(error)));
}
fn on_image_encode_result(&self, task_id: ImageEncodeId, result: std::result::Result<IpcBytes, EncodeError>) {
let mut app = self.write();
app.encoding_images.retain(move |r| {
let done = r.task_id == task_id;
if done {
let _ = r.listener.send_blocking(result.clone());
}
!done
})
}
pub(crate) fn on_message_dlg_response(&self, id: zng_view_api::dialog::DialogId, response: MsgDialogResponse) {
let mut app = self.write();
if let Some(i) = app.message_dialogs.iter().position(|(i, _)| *i == id) {
let (_, r) = app.message_dialogs.swap_remove(i);
r.respond(response);
}
}
pub(crate) fn on_file_dlg_response(&self, id: zng_view_api::dialog::DialogId, response: FileDialogResponse) {
let mut app = self.write();
if let Some(i) = app.file_dialogs.iter().position(|(i, _)| *i == id) {
let (_, r) = app.file_dialogs.swap_remove(i);
r.respond(response);
}
}
pub(crate) fn on_notification_dlg_response(&self, id: zng_view_api::dialog::DialogId, response: NotificationResponse) {
let mut app = self.write();
if let Some(i) = app.notifications.iter().position(|(i, _, _)| *i == id) {
let (_, _, r) = app.notifications.swap_remove(i);
r.respond(response);
}
}
pub(super) fn on_respawned(&self, _gen: ViewProcessGen) {
let mut app = self.write();
app.pending_frames.clear();
for (_, r) in app.message_dialogs.drain(..) {
r.respond(MsgDialogResponse::Error(Txt::from_static("respawn")));
}
for (_, r) in app.file_dialogs.drain(..) {
r.respond(FileDialogResponse::Error(Txt::from_static("respawn")));
}
for (_, _, r) in app.notifications.drain(..) {
r.respond(NotificationResponse::Error(Txt::from_static("respawn")));
}
}
pub(crate) fn exit(&self) {
*VIEW_PROCESS_SV.write() = None;
}
pub(crate) fn ping(&self) {
let mut app = self.write();
let count = app.ping_count.wrapping_add(1);
if let Ok(c) = app.process.ping(count)
&& c != count
{
tracing::error!("incorrect ping response, expected {count}, was {c}");
}
app.ping_count = count;
}
pub(crate) fn on_pong(&self, count: u16) {
let expected = self.read().ping_count;
if expected > count + 1 {
tracing::warn!("unexpected pong event, expected {expected}, was {count}");
}
}
}
impl ViewProcessService {
#[must_use = "if `true` all current WinId, DevId and MonId are invalid"]
fn check_generation(&mut self) -> bool {
let vp_gen = self.process.generation();
let invalid = vp_gen != self.data_generation;
if invalid {
self.data_generation = vp_gen;
self.input_device_ids.clear();
self.monitor_ids.clear();
}
invalid
}
}
event_args! {
pub struct ViewProcessInitedArgs {
pub info: zng_view_api::ViewProcessInfo,
..
fn is_in_target(&self, _id: WidgetId) -> bool {
true
}
}
pub struct ViewProcessSuspendedArgs {
..
fn is_in_target(&self, _id: WidgetId) -> bool {
true
}
}
}
impl std::ops::Deref for ViewProcessInitedArgs {
type Target = zng_view_api::ViewProcessInfo;
fn deref(&self) -> &Self::Target {
&self.info
}
}
event! {
pub static VIEW_PROCESS_INITED_EVENT: ViewProcessInitedArgs;
pub static VIEW_PROCESS_SUSPENDED_EVENT: ViewProcessSuspendedArgs;
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct WindowOpenData {
pub state: WindowStateAll,
pub monitor: Option<MonitorId>,
pub position: (PxPoint, DipPoint),
pub size: DipSize,
pub scale_factor: Factor,
pub refresh_rate: Frequency,
pub render_mode: RenderMode,
pub safe_padding: DipSideOffsets,
}
impl WindowOpenData {
pub(crate) fn new(data: zng_view_api::window::WindowOpenData, map_monitor: impl FnOnce(ApiMonitorId) -> MonitorId) -> Self {
WindowOpenData {
state: data.state,
monitor: data.monitor.map(map_monitor),
position: data.position,
size: data.size,
scale_factor: data.scale_factor,
render_mode: data.render_mode,
safe_padding: data.safe_padding,
refresh_rate: data.refresh_rate,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use = "the window is closed when all clones of the handle are dropped"]
pub struct ViewWindow(ArcEq<ViewWindowData>);
impl ViewWindow {
pub fn generation(&self) -> ViewProcessGen {
self.0.generation
}
pub fn set_title(&self, title: Txt) -> Result<()> {
self.0.call(|id, p| p.set_title(id, title))
}
pub fn set_visible(&self, visible: bool) -> Result<()> {
self.0.call(|id, p| p.set_visible(id, visible))
}
pub fn set_always_on_top(&self, always_on_top: bool) -> Result<()> {
self.0.call(|id, p| p.set_always_on_top(id, always_on_top))
}
pub fn set_movable(&self, movable: bool) -> Result<()> {
self.0.call(|id, p| p.set_movable(id, movable))
}
pub fn set_resizable(&self, resizable: bool) -> Result<()> {
self.0.call(|id, p| p.set_resizable(id, resizable))
}
pub fn set_icon(&self, icon: Option<&ViewImageHandle>) -> Result<()> {
self.0.call(|id, p| {
if let Some(icon) = icon.and_then(|i| i.0.as_ref()) {
if p.generation() == icon.1 {
p.set_icon(id, Some(icon.2))
} else {
Err(ChannelError::disconnected())
}
} else {
p.set_icon(id, None)
}
})
}
pub fn set_cursor(&self, cursor: Option<CursorIcon>) -> Result<()> {
self.0.call(|id, p| p.set_cursor(id, cursor))
}
pub fn set_cursor_image(&self, cursor: Option<&ViewImageHandle>, hotspot: PxPoint) -> Result<()> {
self.0.call(|id, p| {
if let Some(cur) = cursor.and_then(|i| i.0.as_ref()) {
if p.generation() == cur.1 {
p.set_cursor_image(id, Some(zng_view_api::window::CursorImage::new(cur.2, hotspot)))
} else {
Err(ChannelError::disconnected())
}
} else {
p.set_cursor_image(id, None)
}
})
}
pub fn set_taskbar_visible(&self, visible: bool) -> Result<()> {
self.0.call(|id, p| p.set_taskbar_visible(id, visible))
}
pub fn bring_to_top(&self) -> Result<()> {
self.0.call(|id, p| p.bring_to_top(id))
}
pub fn set_state(&self, state: WindowStateAll) -> Result<()> {
self.0.call(|id, p| p.set_state(id, state))
}
pub fn set_video_mode(&self, mode: VideoMode) -> Result<()> {
self.0.call(|id, p| p.set_video_mode(id, mode))
}
pub fn set_enabled_buttons(&self, buttons: WindowButton) -> Result<()> {
self.0.call(|id, p| p.set_enabled_buttons(id, buttons))
}
pub fn renderer(&self) -> ViewRenderer {
ViewRenderer(ArcEq::downgrade(&self.0))
}
pub fn set_capture_mode(&self, enabled: bool) -> Result<()> {
self.0.call(|id, p| p.set_capture_mode(id, enabled))
}
pub fn focus(&self) -> Result<FocusResult> {
self.0.call(|id, p| p.focus(id))
}
pub fn set_focus_indicator(&self, indicator: Option<FocusIndicator>) -> Result<()> {
self.0.call(|id, p| p.set_focus_indicator(id, indicator))
}
pub fn drag_move(&self) -> Result<()> {
self.0.call(|id, p| p.drag_move(id))
}
pub fn drag_resize(&self, direction: ResizeDirection) -> Result<()> {
self.0.call(|id, p| p.drag_resize(id, direction))
}
pub fn start_drag_drop(
&self,
data: Vec<DragDropData>,
allowed_effects: DragDropEffect,
) -> Result<std::result::Result<DragDropId, DragDropError>> {
self.0.call(|id, p| p.start_drag_drop(id, data, allowed_effects))
}
pub fn drag_dropped(&self, drop_id: DragDropId, applied: DragDropEffect) -> Result<()> {
self.0.call(|id, p| p.drag_dropped(id, drop_id, applied))
}
pub fn open_title_bar_context_menu(&self, position: DipPoint) -> Result<()> {
self.0.call(|id, p| p.open_title_bar_context_menu(id, position))
}
pub fn message_dialog(&self, dlg: MsgDialog, responder: ResponderVar<MsgDialogResponse>) -> Result<()> {
let dlg_id = self.0.call(|id, p| p.message_dialog(id, dlg))?;
VIEW_PROCESS.handle_write(self.0.app_id).message_dialogs.push((dlg_id, responder));
Ok(())
}
pub fn file_dialog(&self, dlg: FileDialog, responder: ResponderVar<FileDialogResponse>) -> Result<()> {
let dlg_id = self.0.call(|id, p| p.file_dialog(id, dlg))?;
VIEW_PROCESS.handle_write(self.0.app_id).file_dialogs.push((dlg_id, responder));
Ok(())
}
pub fn access_update(&self, update: zng_view_api::access::AccessTreeUpdate) -> Result<()> {
self.0.call(|id, p| p.access_update(id, update))
}
pub fn set_ime_area(&self, area: Option<DipRect>) -> Result<()> {
self.0.call(|id, p| p.set_ime_area(id, area))
}
pub fn set_system_shutdown_warn(&self, reason: Txt) -> Result<()> {
self.0.call(move |id, p| p.set_system_shutdown_warn(id, reason))
}
pub fn close(self) {
drop(self)
}
pub fn window_extension_raw(&self, extension_id: ApiExtensionId, request: ApiExtensionPayload) -> Result<ApiExtensionPayload> {
self.0.call(|id, p| p.window_extension(id, extension_id, request))
}
pub fn window_extension<I, O>(&self, extension_id: ApiExtensionId, request: &I) -> Result<std::result::Result<O, ApiExtensionRecvError>>
where
I: serde::Serialize,
O: serde::de::DeserializeOwned,
{
let r = self.window_extension_raw(extension_id, ApiExtensionPayload::serialize(&request).unwrap())?;
Ok(r.deserialize())
}
pub fn downgrade(&self) -> WeakViewWindow {
WeakViewWindow(ArcEq::downgrade(&self.0))
}
}
#[derive(Debug, Clone)]
pub struct WeakViewWindow(WeakEq<ViewWindowData>);
impl PartialEq for WeakViewWindow {
fn eq(&self, other: &Self) -> bool {
sync::Weak::ptr_eq(&self.0, &other.0)
}
}
impl Eq for WeakViewWindow {}
impl WeakViewWindow {
pub fn upgrade(&self) -> Option<ViewWindow> {
let d = self.0.upgrade()?;
if d.generation == VIEW_PROCESS.generation() {
Some(ViewWindow(d))
} else {
None
}
}
}
#[derive(Clone, Debug)]
pub enum ViewWindowOrHeadless {
Window(ViewWindow),
Headless(ViewHeadless),
}
impl ViewWindowOrHeadless {
pub fn renderer(&self) -> ViewRenderer {
match self {
ViewWindowOrHeadless::Window(w) => w.renderer(),
ViewWindowOrHeadless::Headless(h) => h.renderer(),
}
}
pub fn window_extension_raw(&self, extension_id: ApiExtensionId, request: ApiExtensionPayload) -> Result<ApiExtensionPayload> {
match self {
ViewWindowOrHeadless::Window(w) => w.window_extension_raw(extension_id, request),
ViewWindowOrHeadless::Headless(h) => h.window_extension_raw(extension_id, request),
}
}
pub fn window_extension<I, O>(&self, extension_id: ApiExtensionId, request: &I) -> Result<std::result::Result<O, ApiExtensionRecvError>>
where
I: serde::Serialize,
O: serde::de::DeserializeOwned,
{
match self {
ViewWindowOrHeadless::Window(w) => w.window_extension(extension_id, request),
ViewWindowOrHeadless::Headless(h) => h.window_extension(extension_id, request),
}
}
}
impl From<ViewWindow> for ViewWindowOrHeadless {
fn from(w: ViewWindow) -> Self {
ViewWindowOrHeadless::Window(w)
}
}
impl From<ViewHeadless> for ViewWindowOrHeadless {
fn from(w: ViewHeadless) -> Self {
ViewWindowOrHeadless::Headless(w)
}
}
#[derive(Debug)]
struct ViewAudioOutputData {
app_id: AppId,
id: ApiAudioOutputId,
generation: ViewProcessGen,
data: AudioOutputOpenData,
}
impl ViewAudioOutputData {
fn call<R>(&self, f: impl FnOnce(ApiAudioOutputId, &mut Controller) -> Result<R>) -> Result<R> {
let mut app = VIEW_PROCESS.handle_write(self.app_id);
if app.check_generation() {
Err(ChannelError::disconnected())
} else {
f(self.id, &mut app.process)
}
}
}
impl Drop for ViewAudioOutputData {
fn drop(&mut self) {
if VIEW_PROCESS.is_available() {
let mut app = VIEW_PROCESS.handle_write(self.app_id);
if self.generation == app.process.generation() {
let _ = app.process.close_audio_output(self.id);
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use = "the audio output is disposed when all clones of the handle are dropped"]
pub struct ViewAudioOutput(ArcEq<ViewAudioOutputData>);
impl ViewAudioOutput {
pub fn cue(&self, mix: AudioMix) -> Result<AudioPlayId> {
self.0.call(|id, p| p.cue_audio(AudioPlayRequest::new(id, mix)))
}
pub fn update(&self, cfg: AudioOutputConfig) -> Result<()> {
self.0.call(|id, p| p.update_audio_output(AudioOutputUpdateRequest::new(id, cfg)))
}
pub fn data(&self) -> &AudioOutputOpenData {
&self.0.data
}
pub fn downgrade(&self) -> WeakViewAudioOutput {
WeakViewAudioOutput(ArcEq::downgrade(&self.0))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WeakViewAudioOutput(WeakEq<ViewAudioOutputData>);
impl WeakViewAudioOutput {
pub fn upgrade(&self) -> Option<ViewAudioOutput> {
let d = self.0.upgrade()?;
if d.generation == VIEW_PROCESS.generation() {
Some(ViewAudioOutput(d))
} else {
None
}
}
}
#[derive(Debug)]
struct ViewWindowData {
app_id: AppId,
id: ApiWindowId,
generation: ViewProcessGen,
}
impl ViewWindowData {
fn call<R>(&self, f: impl FnOnce(ApiWindowId, &mut Controller) -> Result<R>) -> Result<R> {
let mut app = VIEW_PROCESS.handle_write(self.app_id);
if app.check_generation() {
Err(ChannelError::disconnected())
} else {
f(self.id, &mut app.process)
}
}
}
impl Drop for ViewWindowData {
fn drop(&mut self) {
if VIEW_PROCESS.is_available() {
let mut app = VIEW_PROCESS.handle_write(self.app_id);
if self.generation == app.process.generation() {
let _ = app.process.close(self.id);
app.pending_frames.remove(&WindowId::from_raw(self.id.get()));
}
}
}
}
type Result<T> = std::result::Result<T, ChannelError>;
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use = "the view is disposed when all clones of the handle are dropped"]
pub struct ViewHeadless(ArcEq<ViewWindowData>);
impl ViewHeadless {
pub fn set_size(&self, size: DipSize, scale_factor: Factor) -> Result<()> {
self.0.call(|id, p| p.set_headless_size(id, size, scale_factor))
}
pub fn renderer(&self) -> ViewRenderer {
ViewRenderer(ArcEq::downgrade(&self.0))
}
pub fn window_extension_raw(&self, extension_id: ApiExtensionId, request: ApiExtensionPayload) -> Result<ApiExtensionPayload> {
self.0.call(|id, p| p.window_extension(id, extension_id, request))
}
pub fn window_extension<I, O>(&self, extension_id: ApiExtensionId, request: &I) -> Result<std::result::Result<O, ApiExtensionRecvError>>
where
I: serde::Serialize,
O: serde::de::DeserializeOwned,
{
let r = self.window_extension_raw(extension_id, ApiExtensionPayload::serialize(&request).unwrap())?;
Ok(r.deserialize())
}
pub fn downgrade(&self) -> WeakViewHeadless {
WeakViewHeadless(ArcEq::downgrade(&self.0))
}
}
#[derive(Clone, Debug)]
pub struct WeakViewHeadless(WeakEq<ViewWindowData>);
impl PartialEq for WeakViewHeadless {
fn eq(&self, other: &Self) -> bool {
sync::Weak::ptr_eq(&self.0, &other.0)
}
}
impl WeakViewHeadless {
pub fn upgrade(&self) -> Option<ViewHeadless> {
let d = self.0.upgrade()?;
if d.generation == VIEW_PROCESS.generation() {
Some(ViewHeadless(d))
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViewRenderer(WeakEq<ViewWindowData>);
impl ViewRenderer {
fn call<R>(&self, f: impl FnOnce(ApiWindowId, &mut Controller) -> Result<R>) -> Result<R> {
if let Some(c) = self.0.upgrade() {
c.call(f)
} else {
Err(ChannelError::disconnected())
}
}
pub fn generation(&self) -> Result<ViewProcessGen> {
self.0.upgrade().map(|c| c.generation).ok_or(ChannelError::disconnected())
}
pub fn use_image(&self, image: &ViewImageHandle) -> Result<ImageTextureId> {
self.call(|id, p| {
if let Some(img) = &image.0 {
if p.generation() == img.1 {
p.use_image(id, img.2)
} else {
Err(ChannelError::disconnected())
}
} else {
Ok(ImageTextureId::INVALID)
}
})
}
pub fn update_image_use(&mut self, tex_id: ImageTextureId, image: &ViewImageHandle, dirty_rect: Option<PxRect>) -> Result<bool> {
self.call(|id, p| {
if let Some(img) = &image.0 {
if p.generation() == img.1 {
p.update_image_use(id, tex_id, img.2, dirty_rect)
} else {
Err(ChannelError::disconnected())
}
} else {
Ok(false)
}
})
}
pub fn delete_image_use(&mut self, tex_id: ImageTextureId) -> Result<()> {
self.call(|id, p| p.delete_image_use(id, tex_id))
}
pub fn add_font_face(&self, bytes: IpcFontBytes, index: u32) -> Result<FontFaceId> {
self.call(|id, p| p.add_font_face(id, bytes, index))
}
pub fn delete_font_face(&self, font_face_id: FontFaceId) -> Result<()> {
self.call(|id, p| p.delete_font_face(id, font_face_id))
}
pub fn add_font(
&self,
font_face_id: FontFaceId,
glyph_size: Px,
options: FontOptions,
variations: Vec<(FontVariationName, f32)>,
) -> Result<FontId> {
self.call(|id, p| p.add_font(id, font_face_id, glyph_size, options, variations))
}
pub fn delete_font(&self, font_id: FontId) -> Result<()> {
self.call(|id, p| p.delete_font(id, font_id))
}
pub fn frame_image(&self, mask: Option<ImageMaskMode>) -> Result<ViewImageHandle> {
if let Some(c) = self.0.upgrade() {
let id = c.call(|id, p| p.frame_image(id, mask))?;
Ok(Self::add_frame_image(c.app_id, id))
} else {
Err(ChannelError::disconnected())
}
}
pub fn frame_image_rect(&self, rect: PxRect, mask: Option<ImageMaskMode>) -> Result<ViewImageHandle> {
if let Some(c) = self.0.upgrade() {
let id = c.call(|id, p| p.frame_image_rect(id, rect, mask))?;
Ok(Self::add_frame_image(c.app_id, id))
} else {
Err(ChannelError::disconnected())
}
}
fn add_frame_image(app_id: AppId, id: ImageId) -> ViewImageHandle {
if id == ImageId::INVALID {
ViewImageHandle::dummy()
} else {
let mut app = VIEW_PROCESS.handle_write(app_id);
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), id));
app.loading_images.push(ArcEq::downgrade(&handle));
ViewImageHandle(Some(handle))
}
}
pub fn render(&self, frame: FrameRequest) -> Result<()> {
if let Some(w) = self.0.upgrade() {
w.call(|id, p| p.render(id, frame))?;
*VIEW_PROCESS
.handle_write(w.app_id)
.pending_frames
.entry(WindowId::from_raw(w.id.get()))
.or_default() += 1;
Ok(())
} else {
Err(ChannelError::disconnected())
}
}
pub fn render_update(&self, frame: FrameUpdateRequest) -> Result<()> {
if let Some(w) = self.0.upgrade() {
w.call(|id, p| p.render_update(id, frame))?;
*VIEW_PROCESS
.handle_write(w.app_id)
.pending_frames
.entry(WindowId::from_raw(w.id.get()))
.or_default() += 1;
Ok(())
} else {
Err(ChannelError::disconnected())
}
}
pub fn render_extension_raw(&self, extension_id: ApiExtensionId, request: ApiExtensionPayload) -> Result<ApiExtensionPayload> {
if let Some(w) = self.0.upgrade() {
w.call(|id, p| p.render_extension(id, extension_id, request))
} else {
Err(ChannelError::disconnected())
}
}
pub fn render_extension<I, O>(&self, extension_id: ApiExtensionId, request: &I) -> Result<std::result::Result<O, ApiExtensionRecvError>>
where
I: serde::Serialize,
O: serde::de::DeserializeOwned,
{
let r = self.render_extension_raw(extension_id, ApiExtensionPayload::serialize(&request).unwrap())?;
Ok(r.deserialize())
}
}
type ViewImageHandleData = (AppId, ViewProcessGen, ImageId);
#[must_use = "the image is disposed when all clones of the handle are dropped"]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViewImageHandle(Option<ArcEq<ViewImageHandleData>>);
impl ViewImageHandle {
pub fn dummy() -> Self {
ViewImageHandle(None)
}
pub fn is_dummy(&self) -> bool {
self.0.is_none()
}
pub fn image_id(&self) -> ImageId {
self.0.as_ref().map(|h| h.2).unwrap_or(ImageId::INVALID)
}
pub fn app_id(&self) -> Option<AppId> {
self.0.as_ref().map(|h| h.0.0)
}
pub fn view_process_gen(&self) -> ViewProcessGen {
self.0.as_ref().map(|h| h.1).unwrap_or(ViewProcessGen::INVALID)
}
pub fn downgrade(&self) -> WeakViewImageHandle {
match &self.0 {
Some(h) => WeakViewImageHandle(ArcEq::downgrade(h)),
None => WeakViewImageHandle(WeakEq::new()),
}
}
}
impl Drop for ViewImageHandle {
fn drop(&mut self) {
if let Some(h) = self.0.take()
&& Arc::strong_count(&h) == 1
&& let Some(app) = APP.id()
{
if h.0.0 != app {
tracing::error!("image from app `{:?}` dropped in app `{:?}`", h.0, app);
return;
}
if VIEW_PROCESS.is_available() && VIEW_PROCESS.generation() == h.1 {
let _ = VIEW_PROCESS.write().process.forget_image(h.2);
}
}
}
}
#[derive(Clone, Debug)]
pub struct WeakViewImageHandle(WeakEq<ViewImageHandleData>);
impl PartialEq for WeakViewImageHandle {
fn eq(&self, other: &Self) -> bool {
sync::Weak::ptr_eq(&self.0, &other.0)
}
}
impl WeakViewImageHandle {
pub fn upgrade(&self) -> Option<ViewImageHandle> {
let d = self.0.upgrade()?;
if d.1 == VIEW_PROCESS.generation() {
Some(ViewImageHandle(Some(d)))
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncodeError {
Encode(Txt),
Dummy,
Loading,
Disconnected,
}
impl From<Txt> for EncodeError {
fn from(e: Txt) -> Self {
EncodeError::Encode(e)
}
}
impl From<ChannelError> for EncodeError {
fn from(_: ChannelError) -> Self {
EncodeError::Disconnected
}
}
impl fmt::Display for EncodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EncodeError::Encode(e) => write!(f, "{e}"),
EncodeError::Dummy => write!(f, "cannot encode dummy image"),
EncodeError::Loading => write!(f, "cannot encode, image is still loading"),
EncodeError::Disconnected => write!(f, "{}", ChannelError::disconnected()),
}
}
}
impl std::error::Error for EncodeError {}
struct EncodeRequest {
task_id: ImageEncodeId,
listener: channel::Sender<std::result::Result<IpcBytes, EncodeError>>,
}
type ClipboardResult<T> = std::result::Result<T, ClipboardError>;
#[non_exhaustive]
pub struct ViewClipboard {}
impl ViewClipboard {
pub fn read_text(&self) -> Result<ClipboardResult<Txt>> {
match VIEW_PROCESS
.try_write()?
.process
.read_clipboard(vec![ClipboardType::Text], true)?
.map(|mut r| r.pop())
{
Ok(Some(ClipboardData::Text(t))) => Ok(Ok(t)),
Err(e) => Ok(Err(e)),
_ => Ok(Err(ClipboardError::Other(Txt::from_static("view-process returned incorrect type")))),
}
}
pub fn write_text(&self, txt: Txt) -> Result<ClipboardResult<()>> {
VIEW_PROCESS
.try_write()?
.process
.write_clipboard(vec![ClipboardData::Text(txt)])
.map(|r| r.map(|_| ()))
}
pub fn read_image(&self) -> Result<ClipboardResult<ViewImageHandle>> {
let mut app = VIEW_PROCESS.try_write()?;
match app.process.read_clipboard(vec![ClipboardType::Image], true)?.map(|mut r| r.pop()) {
Ok(Some(ClipboardData::Image(id))) => {
if id == ImageId::INVALID {
Ok(Err(ClipboardError::Other(Txt::from_static("view-process returned invalid image"))))
} else {
let handle = ArcEq::new((APP.id().unwrap(), app.process.generation(), id));
app.loading_images.push(ArcEq::downgrade(&handle));
Ok(Ok(ViewImageHandle(Some(handle))))
}
}
Err(e) => Ok(Err(e)),
_ => Ok(Err(ClipboardError::Other(Txt::from_static("view-process returned incorrect type")))),
}
}
pub fn write_image(&self, img: &ViewImageHandle) -> Result<ClipboardResult<()>> {
return VIEW_PROCESS
.try_write()?
.process
.write_clipboard(vec![ClipboardData::Image(img.image_id())])
.map(|r| r.map(|_| ()));
}
pub fn read_paths(&self) -> Result<ClipboardResult<Vec<PathBuf>>> {
match VIEW_PROCESS
.try_write()?
.process
.read_clipboard(vec![ClipboardType::Paths], true)?
.map(|mut r| r.pop())
{
Ok(Some(ClipboardData::Paths(f))) => Ok(Ok(f)),
Err(e) => Ok(Err(e)),
_ => Ok(Err(ClipboardError::Other(Txt::from_static("view-process returned incorrect type")))),
}
}
pub fn write_paths(&self, list: Vec<PathBuf>) -> Result<ClipboardResult<()>> {
VIEW_PROCESS
.try_write()?
.process
.write_clipboard(vec![ClipboardData::Paths(list)])
.map(|r| r.map(|_| ()))
}
pub fn read_extension(&self, data_type: Txt) -> Result<ClipboardResult<IpcBytes>> {
match VIEW_PROCESS
.try_write()?
.process
.read_clipboard(vec![ClipboardType::Extension(data_type.clone())], true)?
.map(|mut r| r.pop())
{
Ok(Some(ClipboardData::Extension { data_type: rt, data })) if rt == data_type => Ok(Ok(data)),
Err(e) => Ok(Err(e)),
_ => Ok(Err(ClipboardError::Other(Txt::from_static("view-process returned incorrect type")))),
}
}
pub fn write_extension(&self, data_type: Txt, data: IpcBytes) -> Result<ClipboardResult<()>> {
VIEW_PROCESS
.try_write()?
.process
.write_clipboard(vec![ClipboardData::Extension { data_type, data }])
.map(|r| r.map(|_| ()))
}
}
type ViewAudioHandleData = (AppId, ViewProcessGen, AudioId);
#[must_use = "the audio is disposed when all clones of the handle are dropped"]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViewAudioHandle(Option<ArcEq<ViewAudioHandleData>>);
impl ViewAudioHandle {
pub fn dummy() -> Self {
ViewAudioHandle(None)
}
pub fn is_dummy(&self) -> bool {
self.0.is_none()
}
pub fn audio_id(&self) -> AudioId {
self.0.as_ref().map(|h| h.2).unwrap_or(AudioId::INVALID)
}
pub fn app_id(&self) -> Option<AppId> {
self.0.as_ref().map(|h| h.0.0)
}
pub fn view_process_gen(&self) -> ViewProcessGen {
self.0.as_ref().map(|h| h.1).unwrap_or(ViewProcessGen::INVALID)
}
pub fn downgrade(&self) -> WeakViewAudioHandle {
match &self.0 {
Some(h) => WeakViewAudioHandle(ArcEq::downgrade(h)),
None => WeakViewAudioHandle(WeakEq::new()),
}
}
}
impl Drop for ViewAudioHandle {
fn drop(&mut self) {
if let Some(h) = self.0.take()
&& Arc::strong_count(&h) == 1
&& let Some(app) = APP.id()
{
if h.0.0 != app {
tracing::error!("audio from app `{:?}` dropped in app `{:?}`", h.0, app);
return;
}
if VIEW_PROCESS.is_available() && VIEW_PROCESS.generation() == h.1 {
let _ = VIEW_PROCESS.write().process.forget_audio(h.2);
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WeakViewAudioHandle(WeakEq<ViewAudioHandleData>);
impl WeakViewAudioHandle {
pub fn upgrade(&self) -> Option<ViewAudioHandle> {
let h = self.0.upgrade()?;
if h.1 == VIEW_PROCESS.generation() {
Some(ViewAudioHandle(Some(h)))
} else {
None
}
}
}
zng_unique_id::unique_id_32! {
pub struct AudioOutputId;
}
zng_unique_id::impl_unique_id_name!(AudioOutputId);
zng_unique_id::impl_unique_id_fmt!(AudioOutputId);
zng_unique_id::impl_unique_id_bytemuck!(AudioOutputId);
zng_var::impl_from_and_into_var! {
fn from(name: &'static str) -> AudioOutputId {
AudioOutputId::named(name)
}
fn from(name: String) -> AudioOutputId {
AudioOutputId::named(name)
}
fn from(name: std::borrow::Cow<'static, str>) -> AudioOutputId {
AudioOutputId::named(name)
}
fn from(name: char) -> AudioOutputId {
AudioOutputId::named(name)
}
fn from(name: Txt) -> AudioOutputId {
AudioOutputId::named(name)
}
fn from(some: AudioOutputId) -> Option<AudioOutputId>;
}
impl serde::Serialize for AudioOutputId {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let name = self.name();
if name.is_empty() {
use serde::ser::Error;
return Err(S::Error::custom("cannot serialize unnamed `AudioOutputId`"));
}
name.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for AudioOutputId {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let name = Txt::deserialize(deserializer)?;
Ok(AudioOutputId::named(name))
}
}