Implemented basic view extensions.

Moved test crash extension directly to the example code.
This commit is contained in:
Samuel Guerra 2023-06-05 18:54:40 -03:00
parent 843f819612
commit 3e4aafb239
5 changed files with 184 additions and 43 deletions

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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"

View File

@ -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
}
}

View File

@ -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)
}
}
}