Implemented basic view extensions.
Moved test crash extension directly to the example code.
This commit is contained in:
parent
843f819612
commit
3e4aafb239
|
@ -1,12 +1,12 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
use zero_ui::core::app::view_process::VIEW_PROCESS;
|
||||
use zero_ui::prelude::*;
|
||||
|
||||
// use zero_ui_view_prebuilt as zero_ui_view;
|
||||
use zero_ui_view::extensions::ViewExtensions;
|
||||
|
||||
fn main() {
|
||||
examples_util::print_info();
|
||||
zero_ui_view::init();
|
||||
zero_ui_view::init_extended(test_extensions);
|
||||
|
||||
App::default().run_window(async {
|
||||
Window! {
|
||||
title = "View-Process Respawn Example";
|
||||
|
@ -31,7 +31,6 @@ fn main() {
|
|||
max_width = 620;
|
||||
},
|
||||
respawn(),
|
||||
#[cfg(debug_assertions)]
|
||||
crash_respawn(),
|
||||
click_counter(),
|
||||
click_counter(),
|
||||
|
@ -51,13 +50,12 @@ fn respawn() -> impl UiNode {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn crash_respawn() -> impl UiNode {
|
||||
Button! {
|
||||
child = Text!("Crash View-Process");
|
||||
on_click = hn!(|_| {
|
||||
if let Ok(ext) = VIEW_PROCESS.extensions() {
|
||||
let crash_ext = zero_ui::core::app::view_process::ApiExtensionName::new("zero-ui-view.crash").unwrap();
|
||||
let crash_ext = zero_ui::core::app::view_process::ApiExtensionName::new("zero-ui.examples.respawn.crash").unwrap();
|
||||
if let Some(key) = ext.key(&crash_ext) {
|
||||
VIEW_PROCESS.extension::<_, ()>(key, &()).unwrap().unwrap();
|
||||
} else {
|
||||
|
@ -130,3 +128,9 @@ fn icon() -> impl UiNode {
|
|||
child = Text!("R");
|
||||
}
|
||||
}
|
||||
|
||||
fn test_extensions() -> ViewExtensions {
|
||||
let mut ext = ViewExtensions::new();
|
||||
ext.command::<(), ()>("zero-ui.examples.respawn.crash", |_| panic!("CRASH"));
|
||||
ext
|
||||
}
|
||||
|
|
|
@ -2191,7 +2191,7 @@ mod serde_debug_flags {
|
|||
/// Note that the bytes here should represent a serialized small `struct` only, you
|
||||
/// can add an [`IpcBytes`] or [`IpcBytesReceiver`] field to this struct to transfer
|
||||
/// large payloads.
|
||||
///
|
||||
///
|
||||
/// [`IpcBytesReceiver`]: crate::ipc::IpcBytesReceiver
|
||||
#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ExtensionPayload(#[serde(with = "serde_bytes")] pub Vec<u8>);
|
||||
|
@ -2240,7 +2240,7 @@ impl ExtensionPayload {
|
|||
///
|
||||
/// if the payload starts with the invalid request header and the key cannot be retrieved the
|
||||
/// `usize::MAX` is returned as the key.
|
||||
///
|
||||
///
|
||||
/// [`unknown_extension`]: Self::unknown_extension
|
||||
pub fn parse_unknown_extension(&self) -> Option<usize> {
|
||||
let p = self.0.strip_prefix(b"zero-ui-view-api.unknown_extension;")?;
|
||||
|
@ -2337,6 +2337,11 @@ impl ops::Deref for ApiExtensionName {
|
|||
self.name.as_str()
|
||||
}
|
||||
}
|
||||
impl From<&'static str> for ApiExtensionName {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self::new(value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// API extension invalid name.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
|
@ -2382,7 +2387,7 @@ impl ApiExtensions {
|
|||
///
|
||||
/// The key can be cached only for the duration of the view process, each view re-instantiation
|
||||
/// must query for the presence of the API extension again, and it may change position on the list.
|
||||
///
|
||||
///
|
||||
/// [`Api::extension`]: crate::Api::extension
|
||||
pub fn key(&self, ext: &ApiExtensionName) -> Option<usize> {
|
||||
self.0.iter().position(|e| e == ext)
|
||||
|
@ -2390,13 +2395,15 @@ impl ApiExtensions {
|
|||
|
||||
/// Push the `ext` to the list, if it is not already inserted.
|
||||
///
|
||||
/// Returns `true` if the extension was inserted.
|
||||
pub fn insert(&mut self, ext: ApiExtensionName) -> bool {
|
||||
let insert = !self.contains(&ext);
|
||||
if insert {
|
||||
/// Returns `Ok(key)` if inserted or `Err(key)` is was already in list.
|
||||
pub fn insert(&mut self, ext: ApiExtensionName) -> Result<usize, usize> {
|
||||
if let Some(key) = self.key(&ext) {
|
||||
Err(key)
|
||||
} else {
|
||||
let key = self.0.len();
|
||||
self.0.push(ext);
|
||||
Ok(key)
|
||||
}
|
||||
insert
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ byteorder = "1"
|
|||
rustc-hash = "1"
|
||||
rayon = "1"
|
||||
backtrace = "0.3"
|
||||
serde = "1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows-sys]
|
||||
version = "0.48.0"
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
//! Extensions API
|
||||
//!
|
||||
//! Extensions that run in the view-process, with internal access to things like the raw handle of windows or
|
||||
//! direct access to renderers. These extensions are build on top of the view API extensions as a way to customize
|
||||
//! the view-process without needing to fork it or re-implement the entire view API from scratch.
|
||||
//!
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
use zero_ui_view_api::{ApiExtensionName, ApiExtensions, ExtensionPayload};
|
||||
|
||||
/// The extension API.
|
||||
pub trait ViewExtension: Send + Any {
|
||||
/// Unique name and version of this extension.
|
||||
fn name(&self) -> &ApiExtensionName;
|
||||
|
||||
/// Run the extension as a command.
|
||||
fn command(&mut self, request: ExtensionPayload) -> Option<ExtensionPayload> {
|
||||
let _ = request;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// View extensions register.
|
||||
#[derive(Default)]
|
||||
pub struct ViewExtensions {
|
||||
ext: Vec<Box<dyn ViewExtension>>,
|
||||
}
|
||||
impl ViewExtensions {
|
||||
/// New empty.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Register an extension.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the name is already registered.
|
||||
pub fn register(&mut self, ext: Box<dyn ViewExtension>) -> &mut Self {
|
||||
if self.is_registered(ext.name()) {
|
||||
panic!("extension `{:?}` is already registered", ext.name());
|
||||
}
|
||||
self.ext.push(ext);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns `true` is an extension of the same name is already registered.
|
||||
pub fn is_registered(&self, name: &ApiExtensionName) -> bool {
|
||||
self.ext.iter().any(|e| e.name() == name)
|
||||
}
|
||||
|
||||
/// Register a command extension with custom encoded messages.
|
||||
pub fn command_raw(
|
||||
&mut self,
|
||||
name: impl Into<ApiExtensionName>,
|
||||
handler: impl FnMut(ExtensionPayload) -> ExtensionPayload + Send + 'static,
|
||||
) -> &mut Self {
|
||||
struct CommandExt<F>(ApiExtensionName, F);
|
||||
impl<F: FnMut(ExtensionPayload) -> ExtensionPayload + Send + 'static> ViewExtension for CommandExt<F> {
|
||||
fn name(&self) -> &ApiExtensionName {
|
||||
&self.0
|
||||
}
|
||||
fn command(&mut self, request: ExtensionPayload) -> Option<ExtensionPayload> {
|
||||
Some((self.1)(request))
|
||||
}
|
||||
}
|
||||
|
||||
self.register(Box::new(CommandExt(name.into(), handler)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a command extension.
|
||||
pub fn command<I: serde::de::DeserializeOwned, O: serde::Serialize>(
|
||||
&mut self,
|
||||
name: impl Into<ApiExtensionName>,
|
||||
mut handler: impl FnMut(I) -> O + Send + 'static,
|
||||
) -> &mut Self {
|
||||
self.command_raw(name, move |i| match i.deserialize::<I>() {
|
||||
Ok(i) => {
|
||||
let o = handler(i);
|
||||
ExtensionPayload::serialize(&o).unwrap()
|
||||
}
|
||||
Err(e) => ExtensionPayload::invalid_request(usize::MAX, e),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn api_extensions(&self) -> ApiExtensions {
|
||||
let mut r = ApiExtensions::new();
|
||||
for ext in &self.ext {
|
||||
r.insert(ext.name().clone()).unwrap();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
pub(crate) fn call_command(&mut self, key: usize, request: ExtensionPayload) -> ExtensionPayload {
|
||||
if key >= self.ext.len() {
|
||||
ExtensionPayload::unknown_extension(key)
|
||||
} else if let Some(r) = self.ext[key].command(request) {
|
||||
r
|
||||
} else {
|
||||
ExtensionPayload::unknown_extension(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, extension_key: usize) -> bool {
|
||||
self.ext.len() > extension_key
|
||||
}
|
||||
}
|
|
@ -86,12 +86,15 @@
|
|||
//! The pre-built crate includes the `"software"` and `"ipc"` features, in fact `ipc` is required, even for running on the same process,
|
||||
//! you can also configure where the pre-build library is installed, see the [`zero-ui-view-prebuilt`] documentation for details.
|
||||
//!
|
||||
//! The pre-build crate does not support [`extensions`].
|
||||
//!
|
||||
//! # API Extensions
|
||||
//!
|
||||
//! This implementation of the view API provides two extensions:
|
||||
//! This implementation of the view API provides one extension:
|
||||
//!
|
||||
//! * `"zero-ui-view.set_webrender_debug"`: `(WindowId, RendererDebug) -> ()`, sets Webrender debug flags.
|
||||
//! * `"zero-ui-view.crash"`: `() -> ()`, only available in debug builds, panics to test the respawn feature.
|
||||
//!
|
||||
//! You can also inject your own extensions, see the [`extensions`] module for more details.
|
||||
//!
|
||||
//! [`glutin`]: https://docs.rs/glutin/
|
||||
//! [`zero-ui-view-prebuilt`]: https://docs.rs/zero-ui-view-prebuilt/
|
||||
|
@ -101,6 +104,7 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use extensions::ViewExtensions;
|
||||
use gl::GlContextManager;
|
||||
use image_cache::ImageCache;
|
||||
use util::WinitToPx;
|
||||
|
@ -119,6 +123,8 @@ mod util;
|
|||
mod window;
|
||||
use surface::*;
|
||||
|
||||
pub mod extensions;
|
||||
|
||||
use webrender::api::*;
|
||||
use window::Window;
|
||||
use zero_ui_view_api::{units::*, *};
|
||||
|
@ -165,6 +171,12 @@ use rustc_hash::FxHashMap;
|
|||
/// event signals, causing the operating system to not detect that the app is frozen.
|
||||
#[cfg(feature = "ipc")]
|
||||
pub fn init() {
|
||||
init_extended(extensions::ViewExtensions::new)
|
||||
}
|
||||
|
||||
/// Like [`init`] but with custom API extensions.
|
||||
#[cfg(feature = "ipc")]
|
||||
pub fn init_extended(ext: fn() -> ViewExtensions) {
|
||||
if !is_main_thread::is_main_thread().unwrap_or(true) {
|
||||
panic!("only call `init` in the main thread, this is a requirement of some operating systems");
|
||||
}
|
||||
|
@ -177,9 +189,9 @@ pub fn init() {
|
|||
let c = connect_view_process(config.server_name).expect("failed to connect to app-process");
|
||||
|
||||
if config.headless {
|
||||
App::run_headless(c);
|
||||
App::run_headless(c, ext());
|
||||
} else {
|
||||
App::run_headed(c);
|
||||
App::run_headed(c, ext());
|
||||
}
|
||||
} else {
|
||||
tracing::trace!("init not in view-process");
|
||||
|
@ -242,6 +254,11 @@ pub extern "C" fn extern_init() {
|
|||
/// event signals, causing the operating system to not detect that the app is frozen. It is **strongly recommended**
|
||||
/// that you build with `panic=abort` or use [`std::panic::set_hook`] to detect these background panics.
|
||||
pub fn run_same_process(run_app: impl FnOnce() + Send + 'static) {
|
||||
run_same_process_extended(run_app, ViewExtensions::new)
|
||||
}
|
||||
|
||||
/// Like [`run_same_process`] but with custom API extensions.
|
||||
pub fn run_same_process_extended(run_app: impl FnOnce() + Send + 'static, ext: fn() -> ViewExtensions) {
|
||||
if !is_main_thread::is_main_thread().unwrap_or(true) {
|
||||
panic!("only call `run_same_process` in the main thread, this is a requirement of some operating systems");
|
||||
}
|
||||
|
@ -254,9 +271,9 @@ pub fn run_same_process(run_app: impl FnOnce() + Send + 'static) {
|
|||
let c = connect_view_process(config.server_name).expect("failed to connect to app in same process");
|
||||
|
||||
if config.headless {
|
||||
App::run_headless(c);
|
||||
App::run_headless(c, ext());
|
||||
} else {
|
||||
App::run_headed(c);
|
||||
App::run_headed(c, ext());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -306,6 +323,9 @@ pub(crate) struct App {
|
|||
|
||||
headless: bool,
|
||||
|
||||
ext: ViewExtensions,
|
||||
webrender_debug_ext: Option<usize>,
|
||||
|
||||
gl_manager: GlContextManager,
|
||||
window_target: *const EventLoopWindowTarget<AppEvent>,
|
||||
app_sender: AppEventSender,
|
||||
|
@ -369,7 +389,7 @@ impl App {
|
|||
util::unregister_raw_input();
|
||||
}
|
||||
|
||||
pub fn run_headless(c: ViewChannels) {
|
||||
pub fn run_headless(c: ViewChannels, ext: ViewExtensions) {
|
||||
tracing::info!("running headless view-process");
|
||||
|
||||
gl::warmup();
|
||||
|
@ -381,6 +401,7 @@ impl App {
|
|||
c.response_sender,
|
||||
c.event_sender,
|
||||
request_receiver,
|
||||
ext,
|
||||
);
|
||||
app.headless = true;
|
||||
|
||||
|
@ -455,7 +476,7 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn run_headed(c: ViewChannels) {
|
||||
pub fn run_headed(c: ViewChannels, ext: ViewExtensions) {
|
||||
tracing::info!("running headed view-process");
|
||||
|
||||
gl::warmup();
|
||||
|
@ -471,6 +492,7 @@ impl App {
|
|||
c.response_sender,
|
||||
c.event_sender,
|
||||
request_receiver,
|
||||
ext,
|
||||
);
|
||||
app.start_receiving(c.request_receiver);
|
||||
|
||||
|
@ -562,10 +584,13 @@ impl App {
|
|||
response_sender: ResponseSender,
|
||||
event_sender: EventSender,
|
||||
request_recv: flume::Receiver<RequestEvent>,
|
||||
ext: ViewExtensions,
|
||||
) -> Self {
|
||||
App {
|
||||
headless: false,
|
||||
started: false,
|
||||
ext,
|
||||
webrender_debug_ext: None,
|
||||
gl_manager: GlContextManager::default(),
|
||||
image_cache: ImageCache::new(app_sender.clone()),
|
||||
app_sender,
|
||||
|
@ -1596,30 +1621,25 @@ impl Api for App {
|
|||
}
|
||||
|
||||
fn extensions(&mut self) -> ApiExtensions {
|
||||
let mut ext = ApiExtensions::new();
|
||||
ext.insert(ApiExtensionName::new("zero-ui-view.set_webrender_debug").unwrap());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
ext.insert(ApiExtensionName::new("zero-ui-view.crash").unwrap());
|
||||
|
||||
ext
|
||||
let mut r = self.ext.api_extensions();
|
||||
if let Ok(k) = r.insert(ApiExtensionName::new("zero-ui-view.set_webrender_debug").unwrap()) {
|
||||
self.webrender_debug_ext = Some(k);
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
fn extension(&mut self, extension_key: usize, extension_request: ExtensionPayload) -> ExtensionPayload {
|
||||
match extension_key {
|
||||
0 => {
|
||||
let (id, dbg) = match extension_request.deserialize::<(WindowId, RendererDebug)>() {
|
||||
Ok(p) => p,
|
||||
Err(e) => return ExtensionPayload::invalid_request(extension_key, &e),
|
||||
};
|
||||
with_window_or_surface!(self, id, |w| w.set_renderer_debug(dbg), || ());
|
||||
ExtensionPayload::empty()
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
1 => {
|
||||
panic!("CRASH")
|
||||
}
|
||||
key => ExtensionPayload::unknown_extension(key),
|
||||
if self.ext.contains(extension_key) {
|
||||
self.ext.call_command(extension_key, extension_request)
|
||||
} else if self.webrender_debug_ext == Some(extension_key) {
|
||||
let (id, dbg) = match extension_request.deserialize::<(WindowId, RendererDebug)>() {
|
||||
Ok(p) => p,
|
||||
Err(e) => return ExtensionPayload::invalid_request(extension_key, &e),
|
||||
};
|
||||
with_window_or_surface!(self, id, |w| w.set_renderer_debug(dbg), || ());
|
||||
ExtensionPayload::empty()
|
||||
} else {
|
||||
ExtensionPayload::unknown_extension(extension_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue