Refactor desktop into App type
This commit is contained in:
15 files changed
+728
-645
No files matched your search
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
|
||||
[dependencies]
|
||||
anyhow = "1.0.56"
|
||||
log = "0.4.11"
|
||||
wry = "0.34.0"
|
||||
wry = "0.35.0"
|
||||
dioxus = { path = "../../packages/dioxus" }
|
||||
dioxus-desktop = { path = "../../packages/desktop", features = [
|
||||
"tokio_runtime",
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
backtrace::Backtrace,
|
||||
cell::RefCell,
|
||||
cell::{Cell, RefCell},
|
||||
error::Error,
|
||||
fmt::{Debug, Display},
|
||||
rc::Rc,
|
||||
@@ -472,8 +472,8 @@ pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element {
|
||||
attr_paths: &[],
|
||||
};
|
||||
VNode {
|
||||
parent: Default::default(),
|
||||
stable_id: Default::default(),
|
||||
parent: Cell::new(None),
|
||||
stable_id: Cell::new(None),
|
||||
key: None,
|
||||
template: std::cell::Cell::new(TEMPLATE),
|
||||
root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(),
|
||||
|
||||
@@ -19,7 +19,10 @@ serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
|
||||
wry = { version = "0.35.0", default-features = false, features = [
|
||||
"protocol",
|
||||
"file-drop",
|
||||
] }
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
@@ -39,11 +42,13 @@ futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
async-trait = "0.1.68"
|
||||
crossbeam-channel = "0.5.8"
|
||||
tao = { version = "0.24.0", features = ["rwh_05"] }
|
||||
muda = "0.11.3"
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||
rfd = "0.12"
|
||||
global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" }
|
||||
global-hotkey = "0.4.1"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
pub use crate::cfg::{Config, WindowCloseBehaviour};
|
||||
pub use crate::desktop_context::DesktopContext;
|
||||
pub use crate::desktop_context::{
|
||||
use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use crate::events::{IpcMessage, KnownIpcMethod};
|
||||
use crate::file_upload;
|
||||
use crate::query::QueryResult;
|
||||
use crate::shortcut::GlobalHotKeyEvent;
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::{event_bubbles, MountedData};
|
||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||
use global_hotkey::{
|
||||
hotkey::{Code, HotKey, Modifiers},
|
||||
GlobalHotKeyManager,
|
||||
};
|
||||
// use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use crate::element::DesktopElement;
|
||||
use crate::eval::init_eval;
|
||||
pub use crate::protocol::{
|
||||
use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse,
|
||||
};
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicU16;
|
||||
use std::task::Waker;
|
||||
use std::{borrow::Borrow, cell::Cell};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||
use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use tao::window::WindowBuilder;
|
||||
use tao::window::WindowId;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::ControlFlow,
|
||||
};
|
||||
use tao::{event_loop::EventLoopBuilder, window::Window};
|
||||
use tokio::runtime::Builder;
|
||||
pub use wry;
|
||||
use wry::WebContext;
|
||||
use wry::WebView;
|
||||
|
||||
pub struct App<P> {
|
||||
// move the props into a cell so we can pop it out later to create the first window
|
||||
// iOS panics if we create a window before the event loop is started
|
||||
pub(crate) props: Rc<Cell<Option<P>>>,
|
||||
pub(crate) cfg: Rc<Cell<Option<Config>>>,
|
||||
pub(crate) root: Component<P>,
|
||||
pub(crate) webviews: HashMap<WindowId, WebviewHandler>,
|
||||
pub(crate) event_handlers: WindowEventHandlers,
|
||||
pub(crate) queue: WebviewQueue,
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
pub(crate) global_hotkey_channel: crossbeam_channel::Receiver<GlobalHotKeyEvent>,
|
||||
pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
|
||||
pub(crate) window_behavior: WindowCloseBehaviour,
|
||||
pub(crate) control_flow: ControlFlow,
|
||||
pub(crate) is_visible_before_start: bool,
|
||||
}
|
||||
|
||||
impl<P> App<P> {
|
||||
pub fn new(cfg: Config, props: P, root: Component<P>) -> (EventLoop<UserWindowEvent>, Self) {
|
||||
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
|
||||
|
||||
let mut app = Self {
|
||||
root,
|
||||
window_behavior: cfg.last_window_close_behaviour,
|
||||
is_visible_before_start: true,
|
||||
webviews: HashMap::new(),
|
||||
event_handlers: WindowEventHandlers::default(),
|
||||
queue: WebviewQueue::default(),
|
||||
shortcut_manager: ShortcutRegistry::new(),
|
||||
global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(),
|
||||
proxy: event_loop.create_proxy(),
|
||||
props: Rc::new(Cell::new(Some(props))),
|
||||
cfg: Rc::new(Cell::new(Some(cfg))),
|
||||
control_flow: ControlFlow::Wait,
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
app.connect_hotreload();
|
||||
|
||||
(event_loop, app)
|
||||
}
|
||||
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
window_event: &Event<'_, UserWindowEvent>,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
// Set the control flow here, but make sure to update it at the end of the match
|
||||
self.control_flow = ControlFlow::Wait;
|
||||
|
||||
self.event_handlers.apply_event(window_event, event_loop);
|
||||
|
||||
if let Ok(event) = self.global_hotkey_channel.try_recv() {
|
||||
self.shortcut_manager.call_handlers(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_hotreload(&mut self) {
|
||||
let proxy = self.proxy.clone();
|
||||
dioxus_hot_reload::connect({
|
||||
move |template| {
|
||||
let _ = proxy.send_event(UserWindowEvent(
|
||||
EventData::HotReloadEvent(template),
|
||||
unsafe { WindowId::dummy() },
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
pub fn handle_new_window(&mut self) {
|
||||
for handler in self.queue.borrow_mut().drain(..) {
|
||||
let id = handler.desktop_context.window.id();
|
||||
self.webviews.insert(id, handler);
|
||||
_ = self.proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_close_requested(&mut self, id: WindowId) {
|
||||
use WindowCloseBehaviour::*;
|
||||
|
||||
match self.window_behavior {
|
||||
LastWindowExitsApp => {
|
||||
self.webviews.remove(&id);
|
||||
if self.webviews.is_empty() {
|
||||
self.control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
|
||||
LastWindowHides => {
|
||||
let Some(webview) = self.webviews.get(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
hide_app_window(&webview.desktop_context.webview);
|
||||
}
|
||||
|
||||
CloseWindow => {
|
||||
self.webviews.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window_destroyed(&mut self, id: WindowId) {
|
||||
self.webviews.remove(&id);
|
||||
|
||||
if matches!(
|
||||
self.window_behavior,
|
||||
WindowCloseBehaviour::LastWindowExitsApp
|
||||
) && self.webviews.is_empty()
|
||||
{
|
||||
self.control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_start_cause_init(&mut self, target: &EventLoopWindowTarget<UserWindowEvent>) {
|
||||
let props = self.props.take().unwrap();
|
||||
let cfg = self.cfg.take().unwrap();
|
||||
|
||||
let dom = VirtualDom::new_with_props(self.root, props);
|
||||
|
||||
self.is_visible_before_start = cfg.window.window.visible;
|
||||
|
||||
let handler = create_new_window(
|
||||
cfg,
|
||||
target,
|
||||
&self.proxy,
|
||||
dom,
|
||||
&self.queue,
|
||||
&self.event_handlers,
|
||||
self.shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = handler.desktop_context.window.id();
|
||||
self.webviews.insert(id, handler);
|
||||
|
||||
_ = self.proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
|
||||
pub fn handle_browser_open(&mut self, msg: IpcMessage) {
|
||||
if let Some(temp) = msg.params().as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let open = webbrowser::open(temp["href"].as_str().unwrap());
|
||||
if let Err(e) = open {
|
||||
tracing::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_initialize_msg(&mut self, id: WindowId) {
|
||||
let view = self.webviews.get_mut(&id).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context);
|
||||
view.desktop_context
|
||||
.window
|
||||
.set_visible(self.is_visible_before_start);
|
||||
}
|
||||
|
||||
pub fn handle_close_msg(&mut self, id: WindowId) {
|
||||
self.webviews.remove(&id);
|
||||
|
||||
if self.webviews.is_empty() {
|
||||
self.control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_poll_msg(&mut self, id: WindowId) {
|
||||
self.poll_vdom(id);
|
||||
}
|
||||
|
||||
pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
|
||||
let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(view) = self.webviews.get(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
view.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query
|
||||
.send(result);
|
||||
}
|
||||
|
||||
pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
|
||||
let params = msg.params();
|
||||
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(params) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
tracing::error!("Error parsing user_event: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let HtmlEvent {
|
||||
element,
|
||||
name,
|
||||
bubbles,
|
||||
data,
|
||||
} = evt;
|
||||
|
||||
let view = self.webviews.get_mut(&id).unwrap();
|
||||
|
||||
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||
let as_any = if let dioxus_html::EventData::Mounted = &data {
|
||||
let query = view
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query
|
||||
.clone();
|
||||
|
||||
let element = DesktopElement::new(element, view.desktop_context.clone(), query);
|
||||
|
||||
Rc::new(MountedData::new(element))
|
||||
} else {
|
||||
data.into_any()
|
||||
};
|
||||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) {
|
||||
match msg {
|
||||
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
|
||||
for webview in self.webviews.values_mut() {
|
||||
webview.dom.replace_template(template);
|
||||
|
||||
// poll_vdom(webview);
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||
self.control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_file_dialog_msg(&self, msg: IpcMessage, window: WindowId) {
|
||||
if let Ok(file_diolog) =
|
||||
serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
|
||||
{
|
||||
let id = ElementId(file_diolog.target);
|
||||
let event_name = &file_diolog.event;
|
||||
let event_bubbles = file_diolog.bubbles;
|
||||
let files = file_upload::get_file_event(&file_diolog);
|
||||
let data = Rc::new(FormData {
|
||||
value: Default::default(),
|
||||
values: Default::default(),
|
||||
files: Some(Arc::new(NativeFileEngine::new(files))),
|
||||
});
|
||||
|
||||
let view = self.webviews.get_mut(&window).unwrap();
|
||||
|
||||
if event_name == "change&input" {
|
||||
view.dom
|
||||
.handle_event("input", data.clone(), id, event_bubbles);
|
||||
view.dom.handle_event("change", data, id, event_bubbles);
|
||||
} else {
|
||||
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||
}
|
||||
|
||||
todo!()
|
||||
// send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll the virtualdom until it's pending
|
||||
///
|
||||
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
||||
///
|
||||
/// All IO is done on the tokio runtime we started earlier
|
||||
pub fn poll_vdom(&mut self, id: WindowId) {
|
||||
let view = self.webviews.get_mut(&id).unwrap();
|
||||
|
||||
let mut cx = std::task::Context::from_waker(&view.waker);
|
||||
|
||||
loop {
|
||||
{
|
||||
let fut = view.dom.wait_for_work();
|
||||
pin_mut!(fut);
|
||||
|
||||
match fut.poll_unpin(&mut cx) {
|
||||
std::task::Poll::Ready(_) => {}
|
||||
std::task::Poll::Pending => break,
|
||||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_new_window(
|
||||
mut cfg: Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: &EventLoopProxy<UserWindowEvent>,
|
||||
dom: VirtualDom,
|
||||
queue: &WebviewQueue,
|
||||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context, asset_handlers, edit_queue, window) =
|
||||
crate::webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
window,
|
||||
webview,
|
||||
proxy.clone(),
|
||||
event_loop.clone(),
|
||||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
edit_queue,
|
||||
asset_handlers,
|
||||
));
|
||||
|
||||
let cx = dom.base_scope();
|
||||
cx.provide_context(desktop_context.clone());
|
||||
|
||||
// Init eval
|
||||
init_eval(cx);
|
||||
|
||||
WebviewHandler {
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
waker: crate::waker::tao_waker(proxy, desktop_context.window.id()),
|
||||
desktop_context,
|
||||
dom,
|
||||
_web_context: web_context,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebviewHandler {
|
||||
dom: VirtualDom,
|
||||
desktop_context: DesktopContext,
|
||||
waker: Waker,
|
||||
|
||||
// Wry assumes the webcontext is alive for the lifetime of the webview.
|
||||
// We need to keep the webcontext alive, otherwise the webview will crash
|
||||
_web_context: WebContext,
|
||||
}
|
||||
|
||||
/// Send a list of mutations to the webview
|
||||
pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
|
||||
let mut channel = desktop_context.channel.borrow_mut();
|
||||
let mut templates = desktop_context.templates.borrow_mut();
|
||||
if let Some(bytes) = apply_edits(
|
||||
edits,
|
||||
&mut channel,
|
||||
&mut templates,
|
||||
&desktop_context.max_template_count,
|
||||
) {
|
||||
desktop_context.edit_queue.add_edits(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Channel {}
|
||||
|
||||
pub fn apply_edits(
|
||||
mutations: Mutations,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &AtomicU16,
|
||||
) -> Option<Vec<u8>> {
|
||||
use dioxus_core::Mutation::*;
|
||||
if mutations.templates.is_empty() && mutations.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for template in mutations.templates {
|
||||
add_template(&template, channel, templates, max_template_count);
|
||||
}
|
||||
for edit in mutations.edits {
|
||||
match edit {
|
||||
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
|
||||
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
|
||||
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
|
||||
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
|
||||
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
|
||||
LoadTemplate { name, index, id } => {
|
||||
if let Some(tmpl_id) = templates.get(name) {
|
||||
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
|
||||
}
|
||||
}
|
||||
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
|
||||
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
|
||||
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
|
||||
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
|
||||
SetAttribute {
|
||||
name,
|
||||
value,
|
||||
id,
|
||||
ns,
|
||||
} => match value {
|
||||
BorrowedAttributeValue::Text(txt) => {
|
||||
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Float(f) => {
|
||||
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Int(n) => {
|
||||
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
|
||||
id.0 as u32,
|
||||
name,
|
||||
if b { "true" } else { "false" },
|
||||
ns.unwrap_or_default(),
|
||||
),
|
||||
BorrowedAttributeValue::None => {
|
||||
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
SetText { value, id } => channel.set_text(id.0 as u32, value),
|
||||
NewEventListener { name, id, .. } => {
|
||||
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
Remove { id } => channel.remove(id.0 as u32),
|
||||
PushRoot { id } => channel.push_root(id.0 as u32),
|
||||
}
|
||||
}
|
||||
|
||||
let bytes: Vec<_> = channel.export_memory().collect();
|
||||
channel.reset();
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
pub fn add_template(
|
||||
template: &Template<'static>,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &AtomicU16,
|
||||
) {
|
||||
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
for root in template.roots.iter() {
|
||||
create_template_node(channel, root);
|
||||
templates.insert(template.name.to_owned(), current_max_template_count);
|
||||
}
|
||||
channel.add_templates(current_max_template_count, template.roots.len() as u16);
|
||||
|
||||
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
|
||||
use TemplateNode::*;
|
||||
match v {
|
||||
Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
// Push the current node onto the stack
|
||||
match namespace {
|
||||
Some(ns) => channel.create_element_ns(tag, ns),
|
||||
None => channel.create_element(tag),
|
||||
}
|
||||
// Set attributes on the current node
|
||||
for attr in *attrs {
|
||||
if let TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} = attr
|
||||
{
|
||||
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
// Add each child to the stack
|
||||
for child in *children {
|
||||
create_template_node(channel, child);
|
||||
}
|
||||
// Add all children to the parent
|
||||
channel.append_children_to_top(children.len() as u16);
|
||||
}
|
||||
Text { text } => channel.create_raw_text(text),
|
||||
DynamicText { .. } => channel.create_raw_text("p"),
|
||||
Dynamic { .. } => channel.add_placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Different hide implementations per platform
|
||||
#[allow(unused)]
|
||||
pub fn hide_app_window(webview: &WebView) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use wry::application::platform::windows::WindowExtWindows;
|
||||
window.set_visible(false);
|
||||
window.set_skip_taskbar(true);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use wry::application::platform::unix::WindowExtUnix;
|
||||
window.set_visible(false);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// window.set_visible(false); has the wrong behaviour on macOS
|
||||
// It will hide the window but not show it again when the user switches
|
||||
// back to the app. `NSApplication::hide:` has the correct behaviour
|
||||
use objc::runtime::Object;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
objc::rc::autoreleasepool(|| unsafe {
|
||||
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
|
||||
let nil = std::ptr::null_mut::<Object>();
|
||||
let _: () = msg_send![app, hide: nil];
|
||||
});
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
@@ -1,11 +1,10 @@
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use wry::application::window::Icon;
|
||||
use tao::window::{Icon, Window, WindowBuilder};
|
||||
use wry::{
|
||||
application::window::{Window, WindowBuilder},
|
||||
http::{Request as HttpRequest, Response as HttpResponse},
|
||||
webview::FileDropEvent,
|
||||
FileDropEvent,
|
||||
};
|
||||
|
||||
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||
@@ -38,7 +37,7 @@ pub struct Config {
|
||||
pub(crate) enable_default_menu_bar: bool,
|
||||
}
|
||||
|
||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
type DropHandler = Box<dyn Fn(FileDropEvent) -> bool>;
|
||||
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
@@ -120,7 +119,7 @@ impl Config {
|
||||
/// Set a file drop handler
|
||||
pub fn with_file_drop_handler(
|
||||
mut self,
|
||||
handler: impl Fn(&Window, FileDropEvent) -> bool + 'static,
|
||||
handler: impl Fn(FileDropEvent) -> bool + 'static,
|
||||
) -> Self {
|
||||
self.file_drop_handler = Some(Box::new(handler));
|
||||
self
|
||||
|
||||
@@ -1,38 +1,29 @@
|
||||
use crate::create_new_window;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::protocol::AssetFuture;
|
||||
use crate::protocol::AssetHandlerRegistry;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
|
||||
use crate::AssetHandler;
|
||||
use crate::Config;
|
||||
use crate::WebviewHandler;
|
||||
use crate::{app::WebviewHandler, events::IpcMessage};
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Formatter;
|
||||
use std::rc::Rc;
|
||||
use std::rc::Weak;
|
||||
use std::sync::atomic::AtomicU16;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
#[cfg(target_os = "ios")]
|
||||
use wry::application::platform::ios::WindowExtIOS;
|
||||
use wry::application::window::Fullscreen as WryFullscreen;
|
||||
use wry::application::window::Window;
|
||||
use wry::application::window::WindowId;
|
||||
use wry::webview::WebView;
|
||||
use std::{
|
||||
cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16,
|
||||
sync::Arc, sync::Mutex,
|
||||
};
|
||||
use tao::{
|
||||
event::Event,
|
||||
event_loop::{EventLoopProxy, EventLoopWindowTarget},
|
||||
window::{Fullscreen as WryFullscreen, Window, WindowId},
|
||||
};
|
||||
|
||||
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
|
||||
use wry::{RequestAsyncResponder, WebView};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
use tao::platform::ios::WindowExtIOS;
|
||||
|
||||
/// Get an imperative handle to the current window without using a hook
|
||||
///
|
||||
@@ -44,7 +35,6 @@ pub fn window() -> DesktopContext {
|
||||
}
|
||||
|
||||
/// Get an imperative handle to the current window
|
||||
#[deprecated = "Prefer the using the `window` function directly for cleaner code"]
|
||||
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
cx.use_hook(|| cx.consume_context::<DesktopContext>())
|
||||
.as_ref()
|
||||
@@ -56,7 +46,7 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct EditQueue {
|
||||
queue: Arc<Mutex<Vec<Vec<u8>>>>,
|
||||
responder: Arc<Mutex<Option<wry::webview::RequestAsyncResponder>>>,
|
||||
responder: Arc<Mutex<Option<RequestAsyncResponder>>>,
|
||||
}
|
||||
|
||||
impl Debug for EditQueue {
|
||||
@@ -71,7 +61,7 @@ impl Debug for EditQueue {
|
||||
}
|
||||
|
||||
impl EditQueue {
|
||||
pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) {
|
||||
pub fn handle_request(&self, responder: RequestAsyncResponder) {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
if let Some(bytes) = queue.pop() {
|
||||
responder.respond(wry::http::Response::new(bytes));
|
||||
@@ -106,10 +96,12 @@ pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
|
||||
/// ```
|
||||
pub struct DesktopService {
|
||||
/// The wry/tao proxy to the current window
|
||||
pub webview: Rc<WebView>,
|
||||
pub webview: WebView,
|
||||
|
||||
pub window: Window,
|
||||
|
||||
/// The proxy to the event loop
|
||||
pub proxy: ProxyType,
|
||||
pub proxy: EventLoopProxy<UserWindowEvent>,
|
||||
|
||||
/// The receiver for queries about the current window
|
||||
pub(super) query: QueryEngine,
|
||||
@@ -141,14 +133,15 @@ impl std::ops::Deref for DesktopService {
|
||||
type Target = Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.webview.window()
|
||||
&self.window
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopService {
|
||||
pub(crate) fn new(
|
||||
window: Window,
|
||||
webview: WebView,
|
||||
proxy: ProxyType,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
event_loop: EventLoopWindowTarget<UserWindowEvent>,
|
||||
webviews: WebviewQueue,
|
||||
event_handlers: WindowEventHandlers,
|
||||
@@ -157,7 +150,8 @@ impl DesktopService {
|
||||
asset_handlers: AssetHandlerRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview: Rc::new(webview),
|
||||
window,
|
||||
webview,
|
||||
proxy,
|
||||
event_loop,
|
||||
query: Default::default(),
|
||||
@@ -198,7 +192,7 @@ impl DesktopService {
|
||||
.consume_context::<Rc<DesktopService>>()
|
||||
.unwrap();
|
||||
|
||||
let id = window.desktop_context.webview.window().id();
|
||||
let id = window.desktop_context.window.id();
|
||||
|
||||
self.proxy
|
||||
.send_event(UserWindowEvent(EventData::NewWindow, id))
|
||||
@@ -222,7 +216,7 @@ impl DesktopService {
|
||||
/// onmousedown: move |_| { desktop.drag_window(); }
|
||||
/// ```
|
||||
pub fn drag(&self) {
|
||||
let window = self.webview.window();
|
||||
let window = &self.window;
|
||||
|
||||
// if the drag_window has any errors, we don't do anything
|
||||
if window.fullscreen().is_none() {
|
||||
@@ -232,7 +226,7 @@ impl DesktopService {
|
||||
|
||||
/// Toggle whether the window is maximized or not
|
||||
pub fn toggle_maximized(&self) {
|
||||
let window = self.webview.window();
|
||||
let window = &self.window;
|
||||
|
||||
window.set_maximized(!window.is_maximized())
|
||||
}
|
||||
@@ -253,10 +247,10 @@ impl DesktopService {
|
||||
|
||||
/// change window to fullscreen
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
if let Some(handle) = self.webview.window().current_monitor() {
|
||||
self.webview
|
||||
.window()
|
||||
.set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle))));
|
||||
if let Some(handle) = &self.window.current_monitor() {
|
||||
self.window.set_fullscreen(
|
||||
fullscreen.then_some(WryFullscreen::Borderless(Some(handle.clone()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +330,7 @@ impl DesktopService {
|
||||
/// Push an objc view to the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
let window = self.webview.window();
|
||||
let window = &self.window;
|
||||
|
||||
unsafe {
|
||||
use objc::runtime::Object;
|
||||
@@ -356,7 +350,7 @@ impl DesktopService {
|
||||
/// Pop an objc view from the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn pop_view(&self) {
|
||||
let window = self.webview.window();
|
||||
let window = &self.window;
|
||||
|
||||
unsafe {
|
||||
use objc::runtime::Object;
|
||||
@@ -380,7 +374,7 @@ pub enum EventData {
|
||||
Ipc(IpcMessage),
|
||||
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
HotReloadEvent(HotReloadMsg),
|
||||
HotReloadEvent(dioxus_hot_reload::HotReloadMsg),
|
||||
|
||||
NewWindow,
|
||||
|
||||
|
||||
@@ -8,9 +8,27 @@ pub struct IpcMessage {
|
||||
params: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub enum KnownIpcMethod<'a> {
|
||||
FileDialog,
|
||||
UserEvent,
|
||||
Query,
|
||||
BrowserOpen,
|
||||
Initialize,
|
||||
Other(&'a str),
|
||||
}
|
||||
|
||||
impl IpcMessage {
|
||||
pub(crate) fn method(&self) -> &str {
|
||||
self.method.as_str()
|
||||
pub(crate) fn method(&self) -> KnownIpcMethod {
|
||||
match self.method.as_str() {
|
||||
// todo: this is a misspelling
|
||||
"file_diolog" => KnownIpcMethod::FileDialog,
|
||||
"user_event" => KnownIpcMethod::UserEvent,
|
||||
"query" => KnownIpcMethod::Query,
|
||||
"browser_open" => KnownIpcMethod::BrowserOpen,
|
||||
"initialize" => KnownIpcMethod::Initialize,
|
||||
_ => KnownIpcMethod::Other(&self.method),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn params(self) -> serde_json::Value {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+34
-516
@@ -3,6 +3,8 @@
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod app;
|
||||
mod asset_handler;
|
||||
mod cfg;
|
||||
mod desktop_context;
|
||||
mod element;
|
||||
@@ -10,6 +12,7 @@ mod escape;
|
||||
mod eval;
|
||||
mod events;
|
||||
mod file_upload;
|
||||
mod hooks;
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
mod protocol;
|
||||
@@ -18,44 +21,24 @@ mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
use crate::query::QueryResult;
|
||||
use crate::shortcut::GlobalHotKeyEvent;
|
||||
pub use cfg::{Config, WindowCloseBehaviour};
|
||||
pub use desktop_context::use_window;
|
||||
pub use desktop_context::DesktopContext;
|
||||
#[allow(deprecated)]
|
||||
pub use desktop_context::{
|
||||
use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use desktop_context::{EventData, UserWindowEvent};
|
||||
use dioxus_core::*;
|
||||
use dioxus_html::{event_bubbles, MountedData};
|
||||
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
||||
use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use element::DesktopElement;
|
||||
use eval::init_eval;
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use events::KnownIpcMethod;
|
||||
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
|
||||
use rustc_hash::FxHashMap;
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicU16;
|
||||
use std::task::Waker;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
pub use tao::dpi::{LogicalSize, PhysicalSize};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
use tao::event::{Event, StartCause, WindowEvent};
|
||||
pub use tao::window::WindowBuilder;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::ControlFlow,
|
||||
};
|
||||
// pub use webview::build_default_menu_bar;
|
||||
use tokio::runtime::Builder;
|
||||
pub use webview::build_default_menu_bar;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::event_loop::EventLoopBuilder;
|
||||
use wry::webview::WebView;
|
||||
use wry::{application::window::WindowId, webview::WebContext};
|
||||
|
||||
/// Launch the WebView and run the event loop.
|
||||
///
|
||||
@@ -127,512 +110,47 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
|
||||
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
let window_behaviour = cfg.last_window_close_behaviour;
|
||||
|
||||
// Intialize hot reloading if it is enabled
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
dioxus_hot_reload::connect({
|
||||
let proxy = proxy.clone();
|
||||
move |template| {
|
||||
let _ = proxy.send_event(UserWindowEvent(
|
||||
EventData::HotReloadEvent(template),
|
||||
unsafe { WindowId::dummy() },
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// We start the tokio runtime *on this thread*
|
||||
// Any future we poll later will use this runtime to spawn tasks and for IO
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
// I would love to just allow dioxus to work with any runtime... but tokio is weird
|
||||
let _guard = Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.enter();
|
||||
|
||||
// We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
|
||||
let _guard = rt.enter();
|
||||
|
||||
// We only have one webview right now, but we'll have more later
|
||||
// Store them in a hashmap so we can remove them when they're closed
|
||||
let mut webviews = HashMap::<WindowId, WebviewHandler>::new();
|
||||
|
||||
// We use this to allow dynamically adding and removing window event handlers
|
||||
let event_handlers = WindowEventHandlers::default();
|
||||
|
||||
let queue = WebviewQueue::default();
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new();
|
||||
let global_hotkey_channel = GlobalHotKeyEvent::receiver();
|
||||
|
||||
// move the props into a cell so we can pop it out later to create the first window
|
||||
// iOS panics if we create a window before the event loop is started
|
||||
let props = Rc::new(Cell::new(Some(props)));
|
||||
let cfg = Rc::new(Cell::new(Some(cfg)));
|
||||
let mut is_visible_before_start = true;
|
||||
let (event_loop, mut app) = app::App::new(cfg, props, root);
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Poll;
|
||||
|
||||
event_handlers.apply_event(&window_event, event_loop);
|
||||
|
||||
if let Ok(event) = global_hotkey_channel.try_recv() {
|
||||
shortcut_manager.call_handlers(event);
|
||||
}
|
||||
app.tick(&window_event, event_loop);
|
||||
|
||||
match window_event {
|
||||
Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(event_loop),
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => match window_behaviour {
|
||||
cfg::WindowCloseBehaviour::LastWindowExitsApp => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
cfg::WindowCloseBehaviour::LastWindowHides => {
|
||||
let Some(webview) = webviews.get(&window_id) else {
|
||||
return;
|
||||
};
|
||||
hide_app_window(&webview.desktop_context.webview);
|
||||
}
|
||||
cfg::WindowCloseBehaviour::CloseWindow => {
|
||||
webviews.remove(&window_id);
|
||||
}
|
||||
},
|
||||
WindowEvent::Destroyed { .. } => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if matches!(
|
||||
window_behaviour,
|
||||
cfg::WindowCloseBehaviour::LastWindowExitsApp
|
||||
) && webviews.is_empty()
|
||||
{
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
WindowEvent::CloseRequested => app.handle_close_requested(window_id),
|
||||
WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
let props = props.take().unwrap();
|
||||
let cfg = cfg.take().unwrap();
|
||||
|
||||
// Create a dom
|
||||
let dom = VirtualDom::new_with_props(root, props);
|
||||
|
||||
is_visible_before_start = cfg.window.window.visible;
|
||||
|
||||
let handler = create_new_window(
|
||||
cfg,
|
||||
event_loop,
|
||||
&proxy,
|
||||
dom,
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = handler.desktop_context.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
|
||||
for handler in queue.borrow_mut().drain(..) {
|
||||
let id = handler.desktop_context.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
}
|
||||
|
||||
Event::UserEvent(event) => match event.0 {
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
EventData::HotReloadEvent(msg) => match msg {
|
||||
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
|
||||
for webview in webviews.values_mut() {
|
||||
webview.dom.replace_template(template);
|
||||
|
||||
poll_vdom(webview);
|
||||
}
|
||||
}
|
||||
dioxus_hot_reload::HotReloadMsg::Shutdown => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
Event::UserEvent(UserWindowEvent(event, id)) => match event {
|
||||
EventData::Poll => app.handle_poll_msg(id),
|
||||
EventData::NewWindow => app.handle_new_window(),
|
||||
EventData::CloseWindow => app.handle_close_msg(id),
|
||||
EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
|
||||
EventData::Ipc(msg) => match msg.method() {
|
||||
KnownIpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
|
||||
KnownIpcMethod::UserEvent => app.handle_user_event_msg(msg, id),
|
||||
KnownIpcMethod::Query => app.handle_query_msg(msg, id),
|
||||
KnownIpcMethod::BrowserOpen => app.handle_browser_open(msg),
|
||||
KnownIpcMethod::Initialize => app.handle_initialize_msg(id),
|
||||
KnownIpcMethod::Other(_) => {}
|
||||
},
|
||||
|
||||
EventData::CloseWindow => {
|
||||
webviews.remove(&event.1);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
|
||||
EventData::Poll => {
|
||||
if let Some(view) = webviews.get_mut(&event.1) {
|
||||
poll_vdom(view);
|
||||
}
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "user_event" => {
|
||||
let params = msg.params();
|
||||
|
||||
let evt = match serde_json::from_value::<HtmlEvent>(params) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
tracing::error!("Error parsing user_event: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let HtmlEvent {
|
||||
element,
|
||||
name,
|
||||
bubbles,
|
||||
data,
|
||||
} = evt;
|
||||
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
|
||||
// check for a mounted event placeholder and replace it with a desktop specific element
|
||||
let as_any = if let dioxus_html::EventData::Mounted = &data {
|
||||
let query = view
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query
|
||||
.clone();
|
||||
|
||||
let element =
|
||||
DesktopElement::new(element, view.desktop_context.clone(), query);
|
||||
|
||||
Rc::new(MountedData::new(element))
|
||||
} else {
|
||||
data.into_any()
|
||||
};
|
||||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
|
||||
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
|
||||
EventData::Ipc(msg) if msg.method() == "query" => {
|
||||
let params = msg.params();
|
||||
|
||||
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
|
||||
let view = webviews.get(&event.1).unwrap();
|
||||
let query = view
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query
|
||||
.clone();
|
||||
|
||||
query.send(result);
|
||||
}
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context);
|
||||
view.desktop_context
|
||||
.webview
|
||||
.window()
|
||||
.set_visible(is_visible_before_start);
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||
if let Some(temp) = msg.params().as_object() {
|
||||
if temp.contains_key("href") {
|
||||
let open = webbrowser::open(temp["href"].as_str().unwrap());
|
||||
if let Err(e) = open {
|
||||
tracing::error!("Open Browser error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
|
||||
if let Ok(file_diolog) =
|
||||
serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
|
||||
{
|
||||
let id = ElementId(file_diolog.target);
|
||||
let event_name = &file_diolog.event;
|
||||
let event_bubbles = file_diolog.bubbles;
|
||||
let files = file_upload::get_file_event(&file_diolog);
|
||||
let data = Rc::new(FormData {
|
||||
value: Default::default(),
|
||||
values: Default::default(),
|
||||
files: Some(Arc::new(NativeFileEngine::new(files))),
|
||||
});
|
||||
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
|
||||
if event_name == "change&input" {
|
||||
view.dom
|
||||
.handle_event("input", data.clone(), id, event_bubbles);
|
||||
view.dom.handle_event("change", data, id, event_bubbles);
|
||||
} else {
|
||||
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Update the control flow
|
||||
*control_flow = app.control_flow;
|
||||
})
|
||||
}
|
||||
|
||||
fn create_new_window(
|
||||
mut cfg: Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: &EventLoopProxy<UserWindowEvent>,
|
||||
dom: VirtualDom,
|
||||
queue: &WebviewQueue,
|
||||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context, asset_handlers, edit_queue) =
|
||||
webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
webview,
|
||||
proxy.clone(),
|
||||
event_loop.clone(),
|
||||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
edit_queue,
|
||||
asset_handlers,
|
||||
));
|
||||
|
||||
let cx = dom.base_scope();
|
||||
cx.provide_context(desktop_context.clone());
|
||||
|
||||
// Init eval
|
||||
init_eval(cx);
|
||||
|
||||
WebviewHandler {
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
waker: waker::tao_waker(proxy, desktop_context.webview.window().id()),
|
||||
desktop_context,
|
||||
dom,
|
||||
_web_context: web_context,
|
||||
}
|
||||
}
|
||||
|
||||
struct WebviewHandler {
|
||||
dom: VirtualDom,
|
||||
desktop_context: DesktopContext,
|
||||
waker: Waker,
|
||||
|
||||
// Wry assumes the webcontext is alive for the lifetime of the webview.
|
||||
// We need to keep the webcontext alive, otherwise the webview will crash
|
||||
_web_context: WebContext,
|
||||
}
|
||||
|
||||
/// Poll the virtualdom until it's pending
|
||||
///
|
||||
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
|
||||
///
|
||||
/// All IO is done on the tokio runtime we started earlier
|
||||
fn poll_vdom(view: &mut WebviewHandler) {
|
||||
let mut cx = std::task::Context::from_waker(&view.waker);
|
||||
|
||||
loop {
|
||||
{
|
||||
let fut = view.dom.wait_for_work();
|
||||
pin_mut!(fut);
|
||||
|
||||
match fut.poll_unpin(&mut cx) {
|
||||
std::task::Poll::Ready(_) => {}
|
||||
std::task::Poll::Pending => break,
|
||||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a list of mutations to the webview
|
||||
fn send_edits(edits: Mutations, desktop_context: &DesktopContext) {
|
||||
let mut channel = desktop_context.channel.borrow_mut();
|
||||
let mut templates = desktop_context.templates.borrow_mut();
|
||||
if let Some(bytes) = apply_edits(
|
||||
edits,
|
||||
&mut channel,
|
||||
&mut templates,
|
||||
&desktop_context.max_template_count,
|
||||
) {
|
||||
desktop_context.edit_queue.add_edits(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_edits(
|
||||
mutations: Mutations,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &AtomicU16,
|
||||
) -> Option<Vec<u8>> {
|
||||
use dioxus_core::Mutation::*;
|
||||
if mutations.templates.is_empty() && mutations.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for template in mutations.templates {
|
||||
add_template(&template, channel, templates, max_template_count);
|
||||
}
|
||||
for edit in mutations.edits {
|
||||
match edit {
|
||||
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
|
||||
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
|
||||
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
|
||||
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
|
||||
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
|
||||
LoadTemplate { name, index, id } => {
|
||||
if let Some(tmpl_id) = templates.get(name) {
|
||||
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
|
||||
}
|
||||
}
|
||||
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
|
||||
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
|
||||
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
|
||||
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
|
||||
SetAttribute {
|
||||
name,
|
||||
value,
|
||||
id,
|
||||
ns,
|
||||
} => match value {
|
||||
BorrowedAttributeValue::Text(txt) => {
|
||||
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Float(f) => {
|
||||
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Int(n) => {
|
||||
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
|
||||
id.0 as u32,
|
||||
name,
|
||||
if b { "true" } else { "false" },
|
||||
ns.unwrap_or_default(),
|
||||
),
|
||||
BorrowedAttributeValue::None => {
|
||||
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
SetText { value, id } => channel.set_text(id.0 as u32, value),
|
||||
NewEventListener { name, id, .. } => {
|
||||
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
Remove { id } => channel.remove(id.0 as u32),
|
||||
PushRoot { id } => channel.push_root(id.0 as u32),
|
||||
}
|
||||
}
|
||||
|
||||
let bytes: Vec<_> = channel.export_memory().collect();
|
||||
channel.reset();
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
fn add_template(
|
||||
template: &Template<'static>,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &AtomicU16,
|
||||
) {
|
||||
let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
for root in template.roots.iter() {
|
||||
create_template_node(channel, root);
|
||||
templates.insert(template.name.to_owned(), current_max_template_count);
|
||||
}
|
||||
channel.add_templates(current_max_template_count, template.roots.len() as u16);
|
||||
|
||||
max_template_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
|
||||
use TemplateNode::*;
|
||||
match v {
|
||||
Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
// Push the current node onto the stack
|
||||
match namespace {
|
||||
Some(ns) => channel.create_element_ns(tag, ns),
|
||||
None => channel.create_element(tag),
|
||||
}
|
||||
// Set attributes on the current node
|
||||
for attr in *attrs {
|
||||
if let TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} = attr
|
||||
{
|
||||
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
// Add each child to the stack
|
||||
for child in *children {
|
||||
create_template_node(channel, child);
|
||||
}
|
||||
// Add all children to the parent
|
||||
channel.append_children_to_top(children.len() as u16);
|
||||
}
|
||||
Text { text } => channel.create_raw_text(text),
|
||||
DynamicText { .. } => channel.create_raw_text("p"),
|
||||
Dynamic { .. } => channel.add_placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Different hide implementations per platform
|
||||
#[allow(unused)]
|
||||
fn hide_app_window(webview: &WebView) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use wry::application::platform::windows::WindowExtWindows;
|
||||
webview.window().set_visible(false);
|
||||
webview.window().set_skip_taskbar(true);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use wry::application::platform::unix::WindowExtUnix;
|
||||
webview.window().set_visible(false);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// webview.window().set_visible(false); has the wrong behaviour on macOS
|
||||
// It will hide the window but not show it again when the user switches
|
||||
// back to the app. `NSApplication::hide:` has the correct behaviour
|
||||
use objc::runtime::Object;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
objc::rc::autoreleasepool(|| unsafe {
|
||||
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
|
||||
let nil = std::ptr::null_mut::<Object>();
|
||||
let _: () = msg_send![app, hide: nil];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{window, DesktopContext};
|
||||
use crate::{use_window, DesktopContext};
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_interpreter_js::INTERPRETER_JS;
|
||||
use slab::Slab;
|
||||
@@ -17,7 +17,7 @@ use tokio::{
|
||||
};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response},
|
||||
Result,
|
||||
RequestAsyncResponder, Result,
|
||||
};
|
||||
|
||||
use crate::desktop_context::EditQueue;
|
||||
@@ -259,8 +259,7 @@ pub(super) async fn desktop_handler(
|
||||
.body(Cow::from(body))
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
return Ok(response);
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
@@ -307,8 +306,7 @@ pub(super) async fn desktop_handler(
|
||||
.body(Cow::from(asset))
|
||||
{
|
||||
Ok(response) => {
|
||||
responder.respond(response);
|
||||
return;
|
||||
return response;
|
||||
}
|
||||
Err(err) => tracing::error!("error building response: {}", err),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
use slab::Slab;
|
||||
use wry::application::keyboard::ModifiersState;
|
||||
use tao::keyboard::ModifiersState;
|
||||
|
||||
use crate::{desktop_context::DesktopContext, window};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::desktop_context::{EventData, UserWindowEvent};
|
||||
use futures_util::task::ArcWake;
|
||||
use futures_util::{pin_mut, task::ArcWake, FutureExt};
|
||||
use std::sync::Arc;
|
||||
use wry::application::{event_loop::EventLoopProxy, window::WindowId};
|
||||
use tao::{event_loop::EventLoopProxy, window::WindowId};
|
||||
|
||||
/// Create a waker that will send a poll event to the event loop.
|
||||
///
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use crate::desktop_context::{EditQueue, EventData};
|
||||
use crate::protocol::{self, AssetHandlerRegistry};
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::window::Window;
|
||||
use tao::window::Window;
|
||||
use wry::http::Response;
|
||||
use wry::webview::{WebContext, WebView, WebViewBuilder};
|
||||
use wry::{WebContext, WebView, WebViewBuilder};
|
||||
|
||||
pub(crate) fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) {
|
||||
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.with_visible(false).build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
@@ -43,15 +41,14 @@ pub(crate) fn build(
|
||||
let asset_handlers = AssetHandlerRegistry::new();
|
||||
let asset_handlers_ref = asset_handlers.clone();
|
||||
|
||||
let mut webview = WebViewBuilder::new(window)
|
||||
.unwrap()
|
||||
let mut webview = WebViewBuilder::new(&window)
|
||||
.with_transparent(cfg.window.window.transparent)
|
||||
.with_url("dioxus://index.html/")
|
||||
.unwrap()
|
||||
.with_ipc_handler(move |window: &Window, payload: String| {
|
||||
.with_ipc_handler(move |payload: String| {
|
||||
// defer the event to the main thread
|
||||
if let Ok(message) = serde_json::from_str(&payload) {
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window_id));
|
||||
}
|
||||
})
|
||||
.with_asynchronous_custom_protocol(String::from("dioxus"), {
|
||||
@@ -77,10 +74,10 @@ pub(crate) fn build(
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
.with_file_drop_handler(move |event| {
|
||||
file_handler
|
||||
.as_ref()
|
||||
.map(|handler| handler(window, evet))
|
||||
.map(|handler| handler(event))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.with_web_context(&mut web_context);
|
||||
@@ -129,63 +126,47 @@ pub(crate) fn build(
|
||||
web_context,
|
||||
asset_handlers,
|
||||
edit_queue,
|
||||
window,
|
||||
)
|
||||
}
|
||||
|
||||
// /// Builds a standard menu bar depending on the users platform. It may be used as a starting point
|
||||
// /// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
|
||||
// /// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
|
||||
// /// > The menu bar differs per platform because of constraints introduced
|
||||
// /// > by [`MenuItem`](tao::menu::MenuItem).
|
||||
// pub fn build_default_menu_bar() -> MenuBar {
|
||||
// let mut menu_bar = MenuBar::new();
|
||||
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
|
||||
/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
|
||||
/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
|
||||
/// > The menu bar differs per platform because of constraints introduced
|
||||
/// > by [`MenuItem`](tao::menu::MenuItem).
|
||||
pub fn build_default_menu_bar() -> Menu {
|
||||
let menu = Menu::new();
|
||||
|
||||
// // since it is uncommon on windows to have an "application menu"
|
||||
// // we add a "window" menu to be more consistent across platforms with the standard menu
|
||||
// let mut window_menu = MenuBar::new();
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// window_menu.add_native_item(MenuItem::EnterFullScreen);
|
||||
// window_menu.add_native_item(MenuItem::Zoom);
|
||||
// window_menu.add_native_item(MenuItem::Separator);
|
||||
// }
|
||||
// since it is uncommon on windows to have an "application menu"
|
||||
// we add a "window" menu to be more consistent across platforms with the standard menu
|
||||
let window_menu = Submenu::new("Window", true);
|
||||
window_menu.append_items(&[
|
||||
&PredefinedMenuItem::fullscreen(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::hide(None),
|
||||
&PredefinedMenuItem::hide_others(None),
|
||||
&PredefinedMenuItem::show_all(None),
|
||||
&PredefinedMenuItem::maximize(None),
|
||||
&PredefinedMenuItem::minimize(None),
|
||||
&PredefinedMenuItem::close_window(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::quit(None),
|
||||
]);
|
||||
|
||||
// window_menu.add_native_item(MenuItem::Hide);
|
||||
let edit_menu = Submenu::new("Window", true);
|
||||
edit_menu.append_items(&[
|
||||
&PredefinedMenuItem::undo(None),
|
||||
&PredefinedMenuItem::redo(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::cut(None),
|
||||
&PredefinedMenuItem::copy(None),
|
||||
&PredefinedMenuItem::paste(None),
|
||||
&PredefinedMenuItem::separator(),
|
||||
&PredefinedMenuItem::select_all(None),
|
||||
]);
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// window_menu.add_native_item(MenuItem::HideOthers);
|
||||
// window_menu.add_native_item(MenuItem::ShowAll);
|
||||
// }
|
||||
menu.append_items(&[&window_menu, &edit_menu]);
|
||||
|
||||
// window_menu.add_native_item(MenuItem::Minimize);
|
||||
// window_menu.add_native_item(MenuItem::CloseWindow);
|
||||
// window_menu.add_native_item(MenuItem::Separator);
|
||||
// window_menu.add_native_item(MenuItem::Quit);
|
||||
// menu_bar.add_submenu("Window", true, window_menu);
|
||||
|
||||
// // since tao supports none of the below items on linux we should only add them on macos/windows
|
||||
// #[cfg(not(target_os = "linux"))]
|
||||
// {
|
||||
// let mut edit_menu = MenuBar::new();
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// edit_menu.add_native_item(MenuItem::Undo);
|
||||
// edit_menu.add_native_item(MenuItem::Redo);
|
||||
// edit_menu.add_native_item(MenuItem::Separator);
|
||||
// }
|
||||
|
||||
// edit_menu.add_native_item(MenuItem::Cut);
|
||||
// edit_menu.add_native_item(MenuItem::Copy);
|
||||
// edit_menu.add_native_item(MenuItem::Paste);
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
// {
|
||||
// edit_menu.add_native_item(MenuItem::Separator);
|
||||
// edit_menu.add_native_item(MenuItem::SelectAll);
|
||||
// }
|
||||
// menu_bar.add_submenu("Edit", true, edit_menu);
|
||||
// }
|
||||
|
||||
// menu_bar
|
||||
// }
|
||||
menu
|
||||
}
|
||||
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.56", optional = true }
|
||||
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
|
||||
sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true }
|
||||
sledgehammer_bindgen = { version = "0.3.0", default-features = false, optional = true }
|
||||
sledgehammer_utils = { version = "0.2", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
|
||||
Reference in new issue
Block a user