Started refactoring events and focus to own crate.

This commit is contained in:
Samuel Guerra 2023-12-06 17:00:42 -03:00
parent 7c0422130d
commit 3544afe838
57 changed files with 13293 additions and 428 deletions

View File

@ -39,8 +39,9 @@ members = [
"zero-ui-ext-l10n",
"zero-ui-ext-image",
"zero-ui-ext-clipboard",
# "zero-ui-ext-undo",
# "zero-ui-ext-window",
"zero-ui-ext-window",
"zero-ui-ext-input",
"zero-ui-ext-undo",
# widget:
"zero-ui-widget",

View File

@ -34,61 +34,33 @@ TextInput! {
# Split Crates
* App crate.
- Everything needed to create an App::minimal, spawn a view-process and render some nodes.
- After it is building do not try to replace in zero-ui-core, just implement the extension
crates too, replace core in main crate when done.
* Implement `TRANSFORM_CHANGED_EVENT` in app crate.
- Currently implemented in notify_transform_changes.
- This tracks all subscriber transforms in a map.
- Need to move this map to the info tree?
- Implement `VISIBILITY_CHANGED_EVENT` in app crate.
- This one is new, visibility was subscribing to FRAME_IMAGE_READY_EVENT.
- Use the same method of storing subscriber data.
- Maybe both transform and visibility changed can be refactored into bulk event like INTERACTIVITY_CHANGED_EVENT.
* Multiple crates need shortcuts only to set on commands.
- Including -app actually, we removed the shortcuts from EXIT_CMD.
- Unfortunately this pulls the entire input and focus stuff.
- Maybe we can have shortcut type in app crate.
- Shortcut is demonstration of command extension, so have it on a crate?
- Can't set on the EXIT_CMD if use crate, because crate will depend on the app for command traits.
* Undo.
- Needs focus scope.
- For `undo_scoped`, really needs it.
- Needs `KEYBOARD` for a config.
- If we unify focus with input this is already a dependency too.
* Focus.
- Needs mouse and touch events.
- Needs WINDOWS.
- Needs gestures and keyboard for shortcut.
* Mouse.
- Needs ModifiersState for keyboard.
- Needs capture.
* Keyboard.
- Needs FOCUS.
* Gesture
- Needs WINDOWS, keyboard and mouse.
* Pointer Capture.
- Needs mouse and touch.
- Needs WINDOWS.
* Focus and input must be on same crate?
- Can decouple from WINDOWS?
* WINDOWS.
- Needs shortcut for command shortcuts.
- Needs LANG_VAR in L10n.
- Needs FOCUS.focused for access and IME stuff.
- Could create a WINDOWS.init_focused_widget(var).
- Needs FONTS.system_font_aa.
- Could listen to the RAW event and record (same thing FONTS does).
* Move widget events to wgt crate.
- Implement `on_show`, `on_collapse`.
* Split main crate into widget crates.
- What about properties?
* Add `WINDOWS.register_root_extender` on the default app?
- `FONTS.system_font_aa`.
- color scheme.
- `lang = LANG_VAR`, for accessibility.
```rust
// removed from core
with_context_var_init(a.root, COLOR_SCHEME_VAR, || WINDOW.vars().actual_color_scheme().boxed()).boxed()
```
* Replace a main crate with a declaration of the default app and manually selected re-exports,
most users should be able to create apps, custom widgets for these apps by simply depending
on this crate. The re-export must be manual so that some stuff that is public does not get re-exported,
things like the view_api `WindowId`, or the `ViewWindow`.
* Delete old core and main crate.
* Test everything.
* Merge.
* Refactor transform and visibility changed events to only send one event per frame like INTERACTIVITY_CHANGED_EVENT.
- Test what happens when info is rebuild,.
# Publish
* Publish if there is no missing component that could cause a core API refactor.

View File

@ -222,11 +222,11 @@ impl<A: EventArgs> Event<A> {
///
/// ```
/// # use zero_ui_app::event::*;
/// # use zero_ui_app::App;
/// # use zero_ui_app::APP;
/// # use zero_ui_app::handler::app_hn;
/// # event_args! { pub struct FocusChangedArgs { pub new_focus: bool, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) {} } }
/// # event! { pub static FOCUS_CHANGED_EVENT: FocusChangedArgs; }
/// # let _scope = App::minimal();
/// # let _scope = APP.minimal();
/// let handle = FOCUS_CHANGED_EVENT.on_pre_event(app_hn!(|args: &FocusChangedArgs, _| {
/// println!("focused: {:?}", args.new_focus);
/// }));
@ -269,11 +269,11 @@ impl<A: EventArgs> Event<A> {
///
/// ```
/// # use zero_ui_app::event::*;
/// # use zero_ui_app::App;
/// # use zero_ui_app::APP;
/// # use zero_ui_app::handler::app_hn;
/// # event_args! { pub struct FocusChangedArgs { pub new_focus: bool, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) {} } }
/// # event! { pub static FOCUS_CHANGED_EVENT: FocusChangedArgs; }
/// # let _scope = App::minimal();
/// # let _scope = APP.minimal();
/// let handle = FOCUS_CHANGED_EVENT.on_event(app_hn!(|args: &FocusChangedArgs, _| {
/// println!("focused: {:?}", args.new_focus);
/// }));

View File

@ -4,7 +4,7 @@ use std::{
mem,
};
use crate::{update::UpdatesTrace, widget::info::WidgetInfo, window::WindowId, App, shortcut::CommandShortcutExt};
use crate::{shortcut::CommandShortcutExt, update::UpdatesTrace, widget::info::WidgetInfo, window::WindowId, APP};
use super::*;
@ -41,7 +41,7 @@ use super::*;
/// You can also initialize metadata:
///
/// ```
/// use zero_ui_app::{event::{command, CommandNameExt, CommandInfoExt}, shortcut::CommandShortcutExt};
/// use zero_ui_app::{event::{command, CommandNameExt, CommandInfoExt}, shortcut::{CommandShortcutExt, shortcut}};
///
/// command! {
/// /// Represents the **foo** action.
@ -58,7 +58,7 @@ use super::*;
/// Or you can use a custom closure to initialize the command:
///
/// ```
/// use zero_ui_app::{event::{command, CommandNameExt, CommandInfoExt}, shortcut::CommandShortcutExt};
/// use zero_ui_app::{event::{command, CommandNameExt, CommandInfoExt}, shortcut::{CommandShortcutExt, shortcut}};
///
/// command! {
/// /// Represents the **foo** action.
@ -598,7 +598,7 @@ impl CommandHandle {
pub fn set_enabled(&self, enabled: bool) {
if let Some(command) = self.command {
if self.local_enabled.swap(enabled, Ordering::Relaxed) != enabled {
if self.app_id != App::current_id() {
if self.app_id != APP.id() {
return;
}
@ -658,7 +658,7 @@ impl fmt::Debug for CommandHandle {
impl Drop for CommandHandle {
fn drop(&mut self) {
if let Some(command) = self.command {
if self.app_id != App::current_id() {
if self.app_id != APP.id() {
return;
}
@ -1121,7 +1121,7 @@ impl CommandData {
CommandHandle {
command: Some(command),
app_id: App::current_id(),
app_id: APP.id(),
local_enabled: AtomicBool::new(enabled),
_event_handle: target.map(|t| command.event.subscribe(t)).unwrap_or_else(EventHandle::dummy),
}
@ -1164,7 +1164,7 @@ mod tests {
#[test]
fn enabled() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
assert!(!FOO_CMD.has_handlers_value());
@ -1184,7 +1184,7 @@ mod tests {
#[test]
fn enabled_scoped() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
let cmd = FOO_CMD;
let cmd_scoped = FOO_CMD.scoped(WindowId::named("enabled_scoped"));
@ -1211,7 +1211,7 @@ mod tests {
#[test]
fn has_handlers() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
assert!(!FOO_CMD.has_handlers_value());
@ -1224,7 +1224,7 @@ mod tests {
#[test]
fn has_handlers_scoped() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
let cmd = FOO_CMD;
let cmd_scoped = FOO_CMD.scoped(WindowId::named("has_handlers_scoped"));

View File

@ -138,7 +138,7 @@ where
/// ```
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::hn;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// # let
/// on_click = hn!(|_| {
@ -153,7 +153,7 @@ where
/// ```
/// # #[derive(Clone)] pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize }
/// # use zero_ui_app::handler::hn;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// # let
/// on_click = hn!(|args: &ClickArgs| {
@ -169,7 +169,7 @@ where
/// # use zero_ui_txt::formatx;
/// # use zero_ui_var::{var, Var};
/// # use zero_ui_app::handler::hn;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// let foo = var(0);
///
@ -248,7 +248,7 @@ where
///
/// ```
/// # use zero_ui_app::handler::hn_once;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<()> {
/// let data = vec![1, 2, 3];
/// # let
@ -265,7 +265,7 @@ where
///
/// ```
/// # use zero_ui_app::handler::hn_once;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # #[derive(Clone)]
/// # pub struct ClickArgs { click_count: usize }
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
@ -354,7 +354,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::async_hn;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// # let
/// on_click = async_hn!(|_| {
@ -376,7 +376,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::async_hn;
/// # use zero_ui_app::widget::WIDGET;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// # let
/// on_click = async_hn!(|args: ClickArgs| {
@ -394,7 +394,7 @@ where
/// # use zero_ui_var::{var, Var};
/// # use zero_ui_task as task;
/// # use zero_ui_txt::formatx;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// let enabled = var(true);
///
@ -524,7 +524,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::async_hn_once;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// let data = vec![1, 2, 3];
/// # let
@ -547,7 +547,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::async_hn_once;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// let data = vec![1, 2, 3];
/// # let
@ -706,7 +706,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # zero_ui_app::event::event! { pub static CLICK_EVENT: ClickArgs; }
/// # use zero_ui_app::handler::app_hn;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// CLICK_EVENT.on_event(app_hn!(|_, _| {
/// println!("Clicked Somewhere!");
@ -724,7 +724,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # zero_ui_app::event::event! { pub static CLICK_EVENT: ClickArgs; }
/// # use zero_ui_app::handler::app_hn;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// CLICK_EVENT.on_event(app_hn!(|args: &ClickArgs, handle| {
/// println!("Clicked {}!", args.target);
@ -741,7 +741,7 @@ where
/// # use zero_ui_txt::{formatx, ToText};
/// # use zero_ui_var::{var, Var};
/// # use zero_ui_app::handler::app_hn;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// let foo = var("".to_text());
///
@ -820,7 +820,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # zero_ui_app::event::event! { pub static CLICK_EVENT: ClickArgs; }
/// # use zero_ui_app::handler::app_hn_once;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// let data = vec![1, 2, 3];
///
@ -839,7 +839,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # zero_ui_app::event::event! { pub static CLICK_EVENT: ClickArgs; }
/// # use zero_ui_app::handler::app_hn_once;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// let data = vec![1, 2, 3];
///
@ -941,7 +941,7 @@ where
/// # zero_ui_app::event::event! { pub static CLICK_EVENT: ClickArgs; }
/// # use zero_ui_app::handler::async_app_hn;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// CLICK_EVENT.on_event(async_app_hn!(|_, _| {
/// println!("Clicked Somewhere!");
@ -967,7 +967,7 @@ where
/// # zero_ui_app::event::event! { pub static CLICK_EVENT: ClickArgs; }
/// # use zero_ui_app::handler::async_app_hn;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// CLICK_EVENT.on_event(async_app_hn!(|args: ClickArgs, handle| {
/// println!("Clicked {}!", args.target);
@ -988,7 +988,7 @@ where
/// # use zero_ui_task as task;
/// # use zero_ui_txt::{formatx, ToText};
/// #
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() {
/// let status = var("pending..".to_text());
///
@ -1108,7 +1108,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::async_hn_once;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// let data = vec![1, 2, 3];
/// # let
@ -1131,7 +1131,7 @@ where
/// # zero_ui_app::event::event_args! { pub struct ClickArgs { pub target: zero_ui_txt::Txt, pub click_count: usize, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # use zero_ui_app::handler::async_hn_once;
/// # use zero_ui_task as task;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// # fn assert_type() -> impl zero_ui_app::handler::WidgetHandler<ClickArgs> {
/// let data = vec![1, 2, 3];
/// # let
@ -1342,7 +1342,7 @@ impl HeadlessApp {
A: Clone + 'static,
H: AppHandler<A>,
{
let mut app = crate::App::minimal().run_headless(false);
let mut app = crate::APP.minimal().run_headless(false);
app.block_on(&mut handler, &args, DOC_TEST_BLOCK_ON_TIMEOUT).unwrap();
}
@ -1355,7 +1355,7 @@ impl HeadlessApp {
where
A: Clone + 'static,
{
let mut app = crate::App::minimal().run_headless(false);
let mut app = crate::APP.minimal().run_headless(false);
app.block_on_multi(handlers.iter_mut().map(|h| h.as_mut()).collect(), &args, DOC_TEST_BLOCK_ON_TIMEOUT)
.unwrap()
}

View File

@ -15,12 +15,14 @@ pub mod access;
pub mod event;
pub mod handler;
pub mod render;
pub mod shortcut;
pub mod timer;
pub mod update;
pub mod view_process;
pub mod widget;
pub mod window;
pub mod shortcut;
mod tests;
// to make the proc-macro $crate substitute work in doc-tests.
#[doc(hidden)]
@ -41,7 +43,7 @@ use window::WindowMode;
use zero_ui_app_context::{AppId, AppScope, LocalContext};
use zero_ui_task::ui::UiTask;
/// An [`App`] extension.
/// An app extension.
///
/// # App Loop
///
@ -81,7 +83,7 @@ use zero_ui_task::ui::UiTask;
/// ## 6 - Deinit
///
/// The [`deinit`] method is called once after an exit was requested and not cancelled. Exit is
/// requested using the [`APP_PROCESS`] service, it causes an [`EXIT_REQUESTED_EVENT`] that can be cancelled, if it
/// requested using the [`APP`] service, it causes an [`EXIT_REQUESTED_EVENT`] that can be cancelled, if it
/// is not cancelled the extensions are deinited and then dropped.
///
/// Deinit happens from the last inited extension first, so in reverse of init order, the [drop] happens in undefined order. Deinit is not called
@ -430,7 +432,7 @@ impl<E: AppExtension> AppExtension for TraceAppExt<E> {
/// Info about an app-extension.
///
/// See [`App::extensions`] for more details.
/// See [`APP::extensions`] for more details.
#[derive(Clone, Copy)]
pub struct AppExtensionInfo {
/// Extension type ID.
@ -676,7 +678,7 @@ impl HeadlessApp {
/// Forces deinit if exit is cancelled.
pub fn exit(mut self) {
self.run_task(async move {
let req = APP_PROCESS.exit();
let req = APP.exit();
req.wait_rsp().await;
});
}
@ -995,21 +997,18 @@ impl AppExtension for Vec<Box<dyn AppExtensionBoxed>> {
}
}
/// Defines and runs an application.
/// Start and manage an app process.
///
/// # View Process
///
/// A view-process must be initialized before creating an app. Panics on `run` if there is
/// A view-process must be initialized before starting an app. Panics on `run` if there is
/// not view-process, also panics if the current process is executing as a view-process.
///
/// [`minimal`]: App::minimal
/// [`default`]: App::default
pub struct App;
impl App {
pub struct APP;
impl APP {
/// If the crate was build with `feature="multi_app"`.
///
/// If `true` multiple apps can run in the same process, but only one app per thread at a time.
pub fn multi_app_enabled() -> bool {
pub fn multi_app_enabled(&self) -> bool {
cfg!(feature = "multi_app")
}
@ -1019,7 +1018,7 @@ impl App {
/// [`AppExtended::run`] returns or the [`HeadlessApp`] is dropped.
///
/// You can use `app_local!` to create *static* resources that live for the app lifetime.
pub fn is_running() -> bool {
pub fn is_running(&self) -> bool {
LocalContext::current_app().is_some()
}
@ -1028,7 +1027,7 @@ impl App {
/// This ID usually does not change as most apps only run once per process, but it can change often during tests.
/// Resources that interact with `app_local!` values can use this ID to ensure that they are still operating in the same
/// app.
pub fn current_id() -> Option<AppId> {
pub fn id(&self) -> Option<AppId> {
LocalContext::current_app()
}
@ -1045,7 +1044,7 @@ impl App {
fn assert_can_run() {
#[cfg(not(feature = "multi_app"))]
Self::assert_can_run_single();
if App::is_running() {
if APP.is_running() {
panic!("only one app is allowed per thread")
}
}
@ -1053,7 +1052,7 @@ impl App {
/// Returns a [`WindowMode`] value that indicates if the app is headless, headless with renderer or headed.
///
/// Note that specific windows can be in headless modes even if the app is headed.
pub fn window_mode() -> WindowMode {
pub fn window_mode(&self) -> WindowMode {
if VIEW_PROCESS.is_available() {
if VIEW_PROCESS.is_headless_with_render() {
WindowMode::HeadlessWithRenderer
@ -1065,15 +1064,15 @@ impl App {
}
}
/// List of app extensions that are part of the current app.
pub fn extensions() -> Arc<AppExtensionsInfo> {
pub fn extensions(&self) -> Arc<AppExtensionsInfo> {
APP_PROCESS_SV.read().extensions()
}
}
impl App {
impl APP {
/// Application without extensions.
#[cfg(dyn_app_extension)]
pub fn minimal() -> AppExtended<Vec<Box<dyn AppExtensionBoxed>>> {
pub fn minimal(&self) -> AppExtended<Vec<Box<dyn AppExtensionBoxed>>> {
assert_not_view_process();
Self::assert_can_run();
check_deadlock();
@ -1086,7 +1085,7 @@ impl App {
}
#[cfg(not(dyn_app_extension))]
pub fn minimal() -> AppExtended<()> {
pub fn minimal(&self) -> AppExtended<()> {
assert_not_view_process();
Self::assert_can_run();
check_deadlock();
@ -1101,7 +1100,7 @@ impl App {
/// Application with extensions.
///
/// See [`App`].
/// See [`APP`].
pub struct AppExtended<E: AppExtension> {
extensions: E,
view_process_exe: Option<PathBuf>,
@ -1209,3 +1208,70 @@ mod private {
// https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed
pub trait Sealed {}
}
/// Sets a `tracing` subscriber that writes warnings to stderr and panics on errors.
///
/// Panics if another different subscriber is already set.
#[cfg(any(test, feature = "test_util"))]
pub fn test_log() {
use std::sync::atomic::*;
use tracing::*;
struct TestSubscriber;
impl Subscriber for TestSubscriber {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
metadata.is_event() && metadata.level() < &Level::WARN
}
fn new_span(&self, _span: &span::Attributes<'_>) -> span::Id {
unimplemented!()
}
fn record(&self, _span: &span::Id, _values: &span::Record<'_>) {
unimplemented!()
}
fn record_follows_from(&self, _span: &span::Id, _follows: &span::Id) {
unimplemented!()
}
fn event(&self, event: &Event<'_>) {
struct MsgCollector<'a>(&'a mut String);
impl<'a> field::Visit for MsgCollector<'a> {
fn record_debug(&mut self, field: &field::Field, value: &dyn fmt::Debug) {
use std::fmt::Write;
write!(self.0, "\n {} = {:?}", field.name(), value).unwrap();
}
}
let meta = event.metadata();
let file = meta.file().unwrap_or("");
let line = meta.line().unwrap_or(0);
let mut msg = format!("[{file}:{line}]");
event.record(&mut MsgCollector(&mut msg));
if meta.level() == &Level::ERROR {
panic!("[LOG-ERROR]{msg}");
} else {
eprintln!("[LOG-WARN]{msg}");
}
}
fn enter(&self, _span: &span::Id) {
unimplemented!()
}
fn exit(&self, _span: &span::Id) {
unimplemented!()
}
}
static IS_SET: AtomicBool = AtomicBool::new(false);
if !IS_SET.swap(true, Ordering::Relaxed) {
if let Err(e) = subscriber::set_global_default(TestSubscriber) {
panic!("failed to set test log subscriber, {e:?}");
}
}
}

View File

@ -438,6 +438,13 @@ impl FrameBuilder {
self.auto_hit_test
}
/// Runs `render` with `aa` used as the default text anti-aliasing mode.
pub fn with_default_font_aa(&mut self, aa: FontRenderMode, render: impl FnOnce(&mut Self)) {
let parent = mem::replace(&mut self.default_font_aa, aa);
render(self);
self.default_font_aa = parent;
}
/// Runs `render` with hit-tests disabled, inside `render` [`is_hit_testable`] is `false`, after
/// it is the current value.
///

View File

@ -25,7 +25,7 @@ use crate::{
view_process::{raw_device_events::DeviceId, *},
widget::WidgetId,
window::WindowId,
AppEventObserver, AppExtension, AppExtensionsInfo, ControlFlow,
AppEventObserver, AppExtension, AppExtensionsInfo, ControlFlow, APP,
};
/// Represents a running app controlled by an external event loop.
@ -980,12 +980,7 @@ impl LoopMonitor {
}
}
/// Service for managing the application process.
///
/// This service is available in all apps.
#[allow(non_camel_case_types)]
pub struct APP_PROCESS;
impl APP_PROCESS {
impl APP {
/// Register a request for process exit with code `0` in the next update.
///
/// The [`EXIT_REQUESTED_EVENT`] will be raised, and if not cancelled the app process will exit.
@ -1002,7 +997,7 @@ impl APP_PROCESS {
command! {
/// Represents the app process [`exit`] request.
///
/// [`exit`]: APP_PROCESS::exit
/// [`exit`]: APP::exit
pub static EXIT_CMD = {
name: "Exit",
info: "Close all windows and exit.",
@ -1071,7 +1066,7 @@ impl AppExtension for AppIntrinsic {
fn event_preview(&mut self, update: &mut EventUpdate) {
if let Some(args) = EXIT_CMD.on(update) {
args.handle_enabled(&self.exit_handle, |_| {
APP_PROCESS.exit();
APP.exit();
});
}
}

View File

@ -754,7 +754,7 @@ impl ModifiersState {
/// If a command has a shortcut the `GestureManager` will invoke the command when the shortcut is pressed
/// the command is enabled, if the command target is a widget it will also be focused. See the `GESTURES`
/// service documentation for details on how shortcuts are resolved.
///
///
/// [`shortcut`]: CommandShortcutExt::shortcut
pub trait CommandShortcutExt {
/// Gets a read-write variable that is zero-or-more shortcuts that invoke the command.
@ -867,7 +867,7 @@ macro_rules! __shortcut {
/// # Examples
///
/// ```
/// use zero_ui_core::gesture::{Shortcut, shortcut};
/// use zero_ui_app::shortcut::{Shortcut, shortcut};
///
/// fn single_key() -> Shortcut {
/// shortcut!(Enter)
@ -892,16 +892,16 @@ macro_rules! __shortcut {
#[macro_export]
macro_rules! shortcut {
(Super) => {
$crate::shortcut::Shortcut::Modifier($crate::gesture::ModifierGesture::Super)
$crate::shortcut::Shortcut::Modifier($crate::shortcut::ModifierGesture::Super)
};
(Shift) => {
$crate::shortcut::Shortcut::Modifier($crate::gesture::ModifierGesture::Shift)
$crate::shortcut::Shortcut::Modifier($crate::shortcut::ModifierGesture::Shift)
};
(Ctrl) => {
$crate::shortcut::Shortcut::Modifier($crate::gesture::ModifierGesture::Ctrl)
$crate::shortcut::Shortcut::Modifier($crate::shortcut::ModifierGesture::Ctrl)
};
(Alt) => {
$crate::shortcut::Shortcut::Modifier($crate::gesture::ModifierGesture::Alt)
$crate::shortcut::Shortcut::Modifier($crate::shortcut::ModifierGesture::Alt)
};
($Key:tt) => {

View File

@ -0,0 +1,9 @@
use zero_ui_app_proc_macros::widget;
#[widget($crate::tests::FooA)]
pub struct Foo(crate::widget::base::WidgetBase);
impl Foo {
pub fn widget_build(&mut self) -> &'static str {
"a"
}
}

View File

@ -0,0 +1,9 @@
use zero_ui_app_proc_macros::widget;
#[widget($crate::tests::FooB)]
pub struct Foo(crate::widget::base::WidgetBase);
impl Foo {
pub fn widget_build(&mut self) -> &'static str {
"b"
}
}

View File

@ -0,0 +1,27 @@
#![cfg(test)]
mod ui_node;
mod widget;
mod a;
mod b;
mod ui_node_list;
pub use a::Foo as FooA;
pub use b::Foo as FooB;
#[test]
fn widget_macro_idents_are_unique() {
// macro_rules! macros are declared in the crate root namespace, so if
// we declare two widgets with the same name in the same crate there is
// a conflict. This is resolved by generating an unique-id from the span.
// This test asserts that even if two widgets with the same name and file span
// are declared, there are still different because the file is different.
let a = FooA!();
let b = FooB!();
assert_eq!("a", a);
assert_eq!("b", b);
}

View File

@ -0,0 +1,377 @@
//! Tests for `#[ui_node(..)]` macro.
//!
//! Note: Compile error tests are in the integration tests folder: `tests/build/ui_node`
use util::{assert_did_not_trace, assert_only_traced, TestTraceNode};
use zero_ui_app_proc_macros::ui_node;
use zero_ui_layout::units::{Px, PxConstraints2d};
use crate::{
ui_vec,
update::WidgetUpdates,
widget::{
instance::{UiNode, UiNodeList},
WidgetUpdateMode, WIDGET,
},
window::WINDOW,
APP,
};
#[test]
pub fn default_child() {
#[ui_node(struct Node { child: impl UiNode })]
impl UiNode for Node {}
test_trace(Node {
child: TestTraceNode::default(),
});
}
#[test]
pub fn default_delegate() {
struct Node<C> {
inner: C,
}
#[ui_node(delegate = &mut self.inner)]
impl<C: UiNode> UiNode for Node<C> {}
test_trace(Node {
inner: TestTraceNode::default(),
});
}
#[test]
pub fn default_children() {
#[ui_node(struct Node { children: impl UiNodeList })]
impl UiNode for Node {}
test_trace(Node {
children: ui_vec![TestTraceNode::default(), TestTraceNode::default()],
});
}
#[test]
pub fn default_delegate_list() {
struct Node<C> {
inner: C,
}
#[ui_node(delegate_list = &mut self.inner)]
impl<C: UiNodeList> UiNode for Node<C> {}
test_trace(Node {
inner: ui_vec![TestTraceNode::default(), TestTraceNode::default()],
});
}
fn test_trace(node: impl UiNode) {
let _app = APP.minimal().run_headless(false);
let mut wgt = util::test_wgt(node);
WINDOW.with_test_context(WidgetUpdateMode::Bubble, || {
WINDOW.test_init(&mut wgt);
assert_only_traced!(wgt, "init");
WINDOW.test_info(&mut wgt);
assert_only_traced!(wgt, "info");
WINDOW.test_update(&mut wgt, None);
assert_only_traced!(wgt, "update");
WINDOW.test_layout(&mut wgt, None);
assert_only_traced!(wgt, "layout");
WINDOW.test_render(&mut wgt);
assert_only_traced!(wgt, "render");
TestTraceNode::notify_render_update(&mut wgt);
assert_only_traced!(wgt, "event");
WINDOW.test_render_update(&mut wgt);
assert_only_traced!(wgt, "render_update");
WINDOW.test_deinit(&mut wgt);
assert_only_traced!(wgt, "deinit");
});
}
#[test]
pub fn allow_missing_delegate() {
#[ui_node(struct Node1 { child: impl UiNode })]
impl UiNode for Node1 {
#[allow_(zero_ui::missing_delegate)]
fn update(&mut self, _: &WidgetUpdates) {
// self.child.update(updates);
}
}
#[ui_node(struct Node2 { child: impl UiNode })]
#[allow_(zero_ui::missing_delegate)]
impl UiNode for Node2 {
fn update(&mut self, _: &WidgetUpdates) {
// self.child.update(updates);
}
}
fn test(node: impl UiNode) {
let _app = APP.minimal().run_headless(false);
let mut wgt = util::test_wgt(node);
WINDOW.with_test_context(WidgetUpdateMode::Bubble, || {
WINDOW.test_init(&mut wgt);
assert_only_traced!(wgt, "init");
WINDOW.test_info(&mut wgt);
assert_only_traced!(wgt, "info");
WINDOW.test_update(&mut wgt, None);
assert_did_not_trace!(wgt);
});
}
test(Node1 {
child: TestTraceNode::default(),
});
test(Node2 {
child: TestTraceNode::default(),
});
}
#[test]
pub fn default_no_child() {
crate::test_log();
#[ui_node(struct Node { })]
impl UiNode for Node {}
let mut wgt = util::test_wgt(Node {});
let _app = APP.minimal().run_headless(false);
WINDOW.with_test_context(WidgetUpdateMode::Bubble, || {
let wu = WINDOW.test_init(&mut wgt);
assert!(wu.info);
assert!(wu.layout);
assert!(wu.render);
let wu = WINDOW.test_info(&mut wgt);
assert!(!wu.info);
assert!(wu.layout);
assert!(wu.render);
let (_, wu) = WINDOW.test_layout(&mut wgt, None);
assert!(!wu.info);
assert!(!wu.layout);
assert!(wu.render);
let (_, wu) = WINDOW.test_render(&mut wgt);
assert!(!wu.info);
assert!(!wu.layout);
assert!(!wu.render);
let wu = WINDOW.test_update(&mut wgt, None);
assert!(!wu.has_updates());
let wu = WINDOW.test_deinit(&mut wgt);
assert!(wu.layout);
assert!(wu.render);
WINDOW.test_init(&mut wgt);
WINDOW.test_info(&mut wgt);
wgt.with_context(WidgetUpdateMode::Ignore, || {
let tree = WINDOW.info();
let wgt_info = tree.get(WIDGET.id()).unwrap();
assert!(wgt_info.descendants().next().is_none());
})
.unwrap();
let constraints = PxConstraints2d::new_unbounded()
.with_min(Px(1), Px(8))
.with_max(Px(100), Px(800))
.with_fill(true, true);
let (desired_size, _) = WINDOW.test_layout(&mut wgt, Some(constraints));
assert_eq!(desired_size, constraints.max_size().unwrap());
let constraints = constraints.with_fill(false, false);
let (desired_size, _) = WINDOW.test_layout(&mut wgt, Some(constraints));
assert_eq!(desired_size, constraints.min_size());
WINDOW.test_render(&mut wgt);
let (update, _) = WINDOW.test_render_update(&mut wgt);
assert!(!update.transforms.is_empty());
assert!(update.floats.is_empty());
assert!(update.colors.is_empty());
assert!(update.clear_color.is_none());
});
}
mod util {
use parking_lot::Mutex;
use std::sync::Arc;
use zero_ui_app_proc_macros::ui_node;
use zero_ui_layout::units::{Px, PxSize};
use zero_ui_state_map::StaticStateId;
pub(super) static TRACE_ID: StaticStateId<Vec<TraceRef>> = StaticStateId::new_unique();
type TraceRef = Arc<Mutex<Vec<&'static str>>>;
/// Asserts that only `method` was traced and clears the trace.
#[macro_export]
macro_rules! __ui_node_util_assert_only_traced {
($wgt:ident, $method:expr) => {{
let method = $method;
$wgt.with_context(WidgetUpdateMode::Bubble, || {
WIDGET.with_state(|s| {
if let Some(db) = s.get(&util::TRACE_ID) {
for (i, trace_ref) in db.iter().enumerate() {
let mut any = false;
for trace_entry in trace_ref.lock().drain(..) {
assert_eq!(trace_entry, method, "tracer_0 traced `{trace_entry}`, expected only `{method}`");
any = true;
}
assert!(any, "tracer_{i} did not trace anything, expected `{method}`");
}
} else {
panic!("no trace initialized, expected `{method}`");
}
})
})
.expect("expected widget");
}};
}
pub use __ui_node_util_assert_only_traced as assert_only_traced;
/// Asserts that no trace entry was pushed.
#[macro_export]
macro_rules! __ui_node_util_assert_did_not_trace {
($wgt:ident) => {{
$wgt.with_context(WidgetUpdateMode::Bubble, || {
WIDGET.with_state(|s| {
if let Some(db) = s.get(&util::TRACE_ID) {
for (i, trace_ref) in db.iter().enumerate() {
let mut any = false;
for trace_entry in trace_ref.lock().iter() {
assert!(any, "tracer_{i} traced `{trace_entry}`, expected nothing");
any = true;
}
}
} else {
panic!("no trace initialized");
}
})
})
.expect("expected widget");
}};
}
pub use __ui_node_util_assert_did_not_trace as assert_did_not_trace;
use crate::{
event::{event, event_args},
render::{FrameBuilder, FrameUpdate},
update::{EventUpdate, UpdateDeliveryList, WidgetUpdates},
widget::{
info::{WidgetInfoBuilder, WidgetLayout, WidgetMeasure},
instance::UiNode,
WidgetId, WidgetUpdateMode, WIDGET,
},
window::WINDOW,
};
#[derive(Default)]
pub struct TestTraceNode {
trace: TraceRef,
}
impl TestTraceNode {
fn test_trace(&self, method: &'static str) {
self.trace.lock().push(method);
}
pub fn notify_render_update(wgt: &mut impl UiNode) {
let id = wgt.with_context(WidgetUpdateMode::Ignore, || WIDGET.id()).expect("expected widget");
let mut update = RENDER_UPDATE_EVENT.new_update_custom(RenderUpdateArgs::now(id), UpdateDeliveryList::new_any());
WINDOW.test_event(wgt, &mut update);
}
}
impl UiNode for TestTraceNode {
fn init(&mut self) {
WIDGET.with_state_mut(|mut s| {
let db = s.entry(&TRACE_ID).or_default();
assert!(db.iter().all(|t| !Arc::ptr_eq(t, &self.trace)), "TraceNode::init called twice");
db.push(Arc::clone(&self.trace));
});
self.test_trace("init");
}
fn info(&mut self, _: &mut WidgetInfoBuilder) {
self.test_trace("info");
}
fn deinit(&mut self) {
self.test_trace("deinit");
}
fn update(&mut self, _: &WidgetUpdates) {
self.test_trace("update");
}
fn event(&mut self, update: &EventUpdate) {
self.test_trace("event");
if RENDER_UPDATE_EVENT.has(update) {
WIDGET.render_update();
}
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
self.test_trace("measure");
PxSize::zero()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
self.test_trace("layout");
PxSize::zero()
}
fn render(&mut self, _: &mut FrameBuilder) {
self.test_trace("render");
}
fn render_update(&mut self, _: &mut FrameUpdate) {
self.test_trace("render_update");
}
}
event_args! {
struct RenderUpdateArgs {
target: WidgetId,
..
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
list.search_widget(self.target);
}
}
}
event! {
static RENDER_UPDATE_EVENT: RenderUpdateArgs;
}
pub fn test_wgt(node: impl UiNode) -> impl UiNode {
MinSizeNode {
child: node,
min_size: PxSize::new(Px(1), Px(1)),
}
.into_widget()
}
struct MinSizeNode<C> {
child: C,
min_size: PxSize,
}
#[ui_node(child)]
impl<C: UiNode> UiNode for MinSizeNode<C> {
fn measure(&mut self, wm: &mut WidgetMeasure) -> PxSize {
self.child.measure(wm).max(self.min_size)
}
fn layout(&mut self, wl: &mut WidgetLayout) -> PxSize {
self.child.layout(wl).max(self.min_size)
}
}
}

View File

@ -0,0 +1,194 @@
use std::collections::HashSet;
use zero_ui_app_proc_macros::{property, widget};
use zero_ui_var::ContextInitHandle;
use crate::{
ui_vec,
widget::{
base::PARALLEL_VAR,
instance::{PanelList, UiNode, UiNodeList, UiNodeVec},
WidgetUpdateMode,
},
window::WINDOW,
APP,
};
use super::widget::EmptyWgt;
#[test]
pub fn init_many() {
let _app = APP.minimal().run_headless(false);
let list: Vec<_> = (0..1000)
.map(|_| {
EmptyWgt! {
util::trace = "inited";
util::log_init_thread = true;
}
.boxed()
})
.collect();
let mut list = PanelList::new(list);
WINDOW.with_test_context(WidgetUpdateMode::Bubble, || {
PARALLEL_VAR.with_context_var(ContextInitHandle::new(), true, || {
list.init_all();
})
});
let mut count = 0;
let mut threads = HashSet::new();
list.for_each(|i, wgt, _| {
assert!(util::traced(wgt, "inited"));
assert_eq!(count, i);
count += 1;
threads.insert(util::get_init_thread(wgt));
});
assert_eq!(count, 1000);
assert!(threads.len() > 1);
}
#[test]
pub fn nested_par_each_ctx() {
let _app = APP.minimal().run_headless(false);
let mut test = ListWgt! {
children = (0..1000)
.map(|_| {
ListWgt! {
children = ui_vec![
EmptyWgt! {
util::ctx_val = true;
util::assert_ctx_val = true;
},
EmptyWgt! {
util::assert_ctx_val = false;
}
];
}
})
.collect::<UiNodeVec>();
};
WINDOW.with_test_context(WidgetUpdateMode::Bubble, || {
WINDOW.test_init(&mut test);
});
}
#[test]
pub fn par_each_ctx() {
let _app = APP.minimal().run_headless(false);
let mut test = ListWgt! {
children = (0..1000)
.flat_map(|_| {
ui_vec![
EmptyWgt! {
util::ctx_val = true;
util::assert_ctx_val = true;
},
EmptyWgt! {
util::assert_ctx_val = false;
}
]
})
.collect::<UiNodeVec>();
};
WINDOW.with_test_context(WidgetUpdateMode::Bubble, || {
WINDOW.test_init(&mut test);
});
}
#[widget($crate::tests::ui_node_list::ListWgt)]
pub struct ListWgt(crate::widget::base::WidgetBase);
impl ListWgt {
fn widget_intrinsic(&mut self) {
self.widget_builder().push_build_action(|wgt| {
let child = util::list_node(wgt.capture_ui_node_list_or_empty(crate::property_id!(Self::children)));
wgt.set_child(child);
});
}
}
#[property(CHILD, capture, widget_impl(ListWgt))]
pub fn children(children: impl UiNodeList) {}
mod util {
use std::{
any::Any,
sync::Arc,
thread::{self, ThreadId},
};
use zero_ui_app_context::{context_local, ContextLocal};
use zero_ui_app_proc_macros::property;
use zero_ui_state_map::StaticStateId;
use zero_ui_var::IntoValue;
use crate::widget::{
instance::{match_node, match_node_list, UiNode, UiNodeList, UiNodeOp},
WidgetUpdateMode, WIDGET,
};
pub use super::super::widget::util::*;
#[property(CONTEXT)]
pub fn log_init_thread(child: impl UiNode, enabled: impl IntoValue<bool>) -> impl UiNode {
let enabled = enabled.into();
match_node(child, move |child, op| {
if let UiNodeOp::Init = op {
child.init();
if enabled {
WIDGET.set_state(&INIT_THREAD_ID, thread::current().id());
}
}
})
}
pub fn get_init_thread(wgt: &mut impl UiNode) -> ThreadId {
wgt.with_context(WidgetUpdateMode::Ignore, || {
WIDGET.get_state(&INIT_THREAD_ID).expect("did not log init thread")
})
.expect("node is not an widget")
}
static INIT_THREAD_ID: StaticStateId<ThreadId> = StaticStateId::new_unique();
context_local! {
static CTX_VAL: bool = false;
}
#[property(CONTEXT, default(*CTX_VAL.get()))]
pub fn ctx_val(child: impl UiNode, value: impl IntoValue<bool>) -> impl UiNode {
with_context_local(child, &CTX_VAL, value)
}
#[property(CHILD)]
pub fn assert_ctx_val(child: impl UiNode, expected: impl IntoValue<bool>) -> impl UiNode {
let expected = expected.into();
match_node(child, move |child, op| {
if let UiNodeOp::Init = op {
child.init();
// thread::sleep(1.ms());
assert_eq!(expected, *CTX_VAL.get());
}
})
}
pub fn list_node(children: impl UiNodeList) -> impl UiNode {
match_node_list(children, |_, _| {})
}
fn with_context_local<T: Any + Send + Sync + 'static>(
child: impl UiNode,
context: &'static ContextLocal<T>,
value: impl Into<T>,
) -> impl UiNode {
let mut value = Some(Arc::new(value.into()));
match_node(child, move |child, op| {
context.with_context(&mut value, || child.op(op));
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ use crate::{
handler::{async_app_hn_once, AppHandler, AppHandlerArgs, AppWeakHandle},
timer::TIMERS_SV,
widget::{
info::{WidgetInfo, WidgetInfoTree, WidgetPath},
info::{InteractionPath, WidgetInfo, WidgetInfoTree, WidgetPath},
instance::{BoxedUiNode, UiNode},
WidgetId, WIDGET,
},
@ -236,6 +236,17 @@ impl WidgetPathProvider for WidgetPath {
self.widgets_path().iter().copied().rev()
}
}
impl WidgetPathProvider for InteractionPath {
type WidgetIter<'s> = std::iter::Rev<std::iter::Copied<std::slice::Iter<'s, WidgetId>>>;
fn window_id(&self) -> WindowId {
WidgetPath::window_id(self)
}
fn widget_and_ancestors(&self) -> Self::WidgetIter<'_> {
self.widgets_path().iter().copied().rev()
}
}
/// Represents a set of widgets that subscribe to an event source.
pub trait UpdateSubscribers: Send + Sync + 'static {

View File

@ -49,7 +49,7 @@ pub(crate) use zero_ui_view_api::{
use self::raw_device_events::DeviceId;
use super::{App, AppId};
use super::{AppId, APP};
/// Connection to the running view-process for the context app.
#[allow(non_camel_case_types)]
@ -79,7 +79,7 @@ impl VIEW_PROCESS {
/// If the `VIEW_PROCESS` can be used, this is only true in app threads for apps with render, all other
/// methods will panic if called when this is not true.
pub fn is_available(&self) -> bool {
App::is_running() && VIEW_PROCESS_SV.read().is_some()
APP.is_running() && VIEW_PROCESS_SV.read().is_some()
}
fn read(&self) -> MappedRwLockReadGuard<ViewProcessService> {
@ -101,7 +101,7 @@ impl VIEW_PROCESS {
}
fn check_app(&self, id: AppId) {
let actual = App::current_id();
let actual = APP.id();
if Some(id) != actual {
panic!("cannot use view handle from app `{id:?}` in app `{actual:?}`");
}
@ -166,7 +166,7 @@ impl VIEW_PROCESS {
let id = app.process.add_image(request)?;
let img = ViewImage(Arc::new(RwLock::new(ViewImageData {
id: Some(id),
app_id: App::current_id(),
app_id: APP.id(),
generation: app.process.generation(),
size: PxSize::zero(),
partial_size: PxSize::zero(),
@ -191,7 +191,7 @@ impl VIEW_PROCESS {
let id = app.process.add_image_pro(request)?;
let img = ViewImage(Arc::new(RwLock::new(ViewImageData {
id: Some(id),
app_id: App::current_id(),
app_id: APP.id(),
generation: app.process.generation(),
size: PxSize::zero(),
partial_size: PxSize::zero(),
@ -310,7 +310,7 @@ impl VIEW_PROCESS {
let _ = app.check_generation();
let win = ViewWindow(Arc::new(ViewWindowData {
app_id: App::current_id().unwrap(),
app_id: APP.id().unwrap(),
id: ApiWindowId::from_raw(window_id.get()),
id_namespace: data.id_namespace,
pipeline_id: data.pipeline_id,
@ -350,7 +350,7 @@ impl VIEW_PROCESS {
let _ = app.check_generation();
let surf = ViewHeadless(Arc::new(ViewWindowData {
app_id: App::current_id().unwrap(),
app_id: APP.id().unwrap(),
id: ApiWindowId::from_raw(id.get()),
id_namespace: data.id_namespace,
pipeline_id: data.pipeline_id,
@ -450,7 +450,7 @@ impl VIEW_PROCESS {
pub(crate) fn on_frame_image(&self, data: ImageLoadedData) -> ViewImage {
ViewImage(Arc::new(RwLock::new(ViewImageData {
app_id: App::current_id(),
app_id: APP.id(),
id: Some(data.id),
generation: self.generation(),
size: data.size,
@ -1114,7 +1114,7 @@ impl Drop for ViewImageData {
fn drop(&mut self) {
if let Some(id) = self.id {
let app_id = self.app_id.unwrap();
if let Some(app) = App::current_id() {
if let Some(app) = APP.id() {
if app_id != app {
tracing::error!("image from app `{:?}` dropped in app `{:?}`", app_id, app);
}
@ -1384,7 +1384,7 @@ impl ViewClipboard {
} else {
let img = ViewImage(Arc::new(RwLock::new(ViewImageData {
id: Some(id),
app_id: App::current_id(),
app_id: APP.id(),
generation: app.process.generation(),
size: PxSize::zero(),
partial_size: PxSize::zero(),

View File

@ -1048,7 +1048,7 @@ pub use zero_ui_app_proc_macros::widget_mixin;
/// # #[property(CONTEXT)] pub fn background_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode { child }
/// # #[property(EVENT)] pub fn is_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode { child }
/// # fn main() {
/// # let _scope = App::minimal();
/// # let _scope = APP.minimal();
/// # let wgt = zero_ui_app::widget::base::WidgetBase! {
/// background_color = colors::RED;
///
@ -1101,7 +1101,7 @@ pub use zero_ui_app_proc_macros::widget_mixin;
/// pub static BAR_VAR: bool = false;
/// }
/// # fn main() {
/// # let _scope = App::minimal();
/// # let _scope = APP.minimal();
/// # let wgt = widget::base::WidgetBase! {
/// background_color = colors::RED;
///

View File

@ -34,7 +34,7 @@ use zero_ui_layout::{
};
use zero_ui_state_map::{OwnedStateMap, StateMapRef};
use zero_ui_txt::{formatx, Txt};
use zero_ui_unique_id::IdMap;
use zero_ui_unique_id::{IdEntry, IdMap};
use zero_ui_var::impl_from_and_into_var;
use zero_ui_view_api::{display_list::FrameValueUpdate, window::FrameId};
@ -142,6 +142,9 @@ struct WidgetInfoTreeFrame {
spatial_bounds: PxBox,
widget_count_offsets: ParallelSegmentOffsets,
transform_changed_subs: IdMap<WidgetId, PxTransform>,
visibility_changed_subs: IdMap<WidgetId, Visibility>,
}
impl PartialEq for WidgetInfoTree {
fn eq(&self, other: &Self) -> bool {
@ -287,6 +290,54 @@ impl WidgetInfoTree {
if let Some(w) = widget_count_offsets {
frame.widget_count_offsets = w;
}
let mut changes_count = 0;
TRANSFORM_CHANGED_EVENT.visit_subscribers(|wid| {
if let Some(wgt) = self.get(wid) {
let transform = wgt.bounds_info().inner_transform();
match frame.transform_changed_subs.entry(wid) {
IdEntry::Occupied(mut e) => {
let prev = e.insert(transform);
if prev != transform {
TRANSFORM_CHANGED_EVENT.notify(TransformChangedArgs::now(wgt.path(), prev, transform));
changes_count += 1;
}
}
IdEntry::Vacant(e) => {
e.insert(transform);
}
}
}
});
if (frame.transform_changed_subs.len() - changes_count) > 500 {
frame
.transform_changed_subs
.retain(|k, _| TRANSFORM_CHANGED_EVENT.is_subscriber(*k));
}
changes_count = 0;
VISIBILITY_CHANGED_EVENT.visit_subscribers(|wid| {
if let Some(wgt) = self.get(wid) {
let visibility = wgt.visibility();
match frame.visibility_changed_subs.entry(wid) {
IdEntry::Occupied(mut e) => {
let prev = e.insert(visibility);
if prev != visibility {
VISIBILITY_CHANGED_EVENT.notify(VisibilityChangedArgs::now(wgt.path(), prev, visibility));
changes_count += 1;
}
}
IdEntry::Vacant(e) => {
e.insert(visibility);
}
}
}
});
if (frame.visibility_changed_subs.len() - changes_count) > 500 {
frame
.visibility_changed_subs
.retain(|k, _| VISIBILITY_CHANGED_EVENT.is_subscriber(*k));
}
}
pub(crate) fn after_render_update(&self, frame_id: FrameId) {

View File

@ -343,6 +343,8 @@ impl WidgetInfoBuilder {
scale_factor: self.scale_factor,
spatial_bounds,
widget_count_offsets,
transform_changed_subs: IdMap::new(),
visibility_changed_subs: IdMap::new(),
}),
tree: self.tree,

View File

@ -444,7 +444,7 @@ mod tests {
WidgetCtx, WidgetId, WidgetUpdateMode, WIDGET,
},
window::{WindowId, WINDOW},
App,
APP,
};
trait WidgetInfoBuilderExt {
@ -475,7 +475,7 @@ mod tests {
}
fn data() -> WidgetInfoTree {
let _scope = App::minimal();
let _scope = APP.minimal();
let mut builder = WidgetInfoBuilder::new(
Arc::default(),
WindowId::named("w"),
@ -610,7 +610,7 @@ mod tests {
}
fn data_nested() -> WidgetInfoTree {
let _scope = App::minimal();
let _scope = APP.minimal();
let mut builder = WidgetInfoBuilder::new(
Arc::default(),
WindowId::named("w"),
@ -733,7 +733,7 @@ mod tests {
}
fn data_deep() -> WidgetInfoTree {
let _scope = App::minimal();
let _scope = APP.minimal();
let mut builder = WidgetInfoBuilder::new(
Arc::default(),
WindowId::named("w"),

View File

@ -24,12 +24,6 @@ use zero_ui_view_api::ipc::IpcBytes;
/// Services provided by this extension.
///
/// * [`CLIPBOARD`]
///
/// # Default
///
/// This extension is included in the [default app].
///
/// [default app]: crate::app::App::default
#[derive(Default)]
pub struct ClipboardManager {}

View File

@ -125,14 +125,15 @@ impl Default for SwapConfig {
#[cfg(test)]
mod tests {
use zero_ui_app::App;
use zero_ui_app::APP;
use zero_ui_ext_fs_watcher::FsWatcherManager;
use super::*;
#[test]
fn swap_config_in_memory() {
let mut app = App::minimal()
let mut app = APP
.minimal()
.extend(FsWatcherManager::default())
.extend(ConfigManager::default())
.run_headless(false);
@ -151,7 +152,8 @@ mod tests {
#[test]
fn swap_config_swap() {
let mut app = App::minimal()
let mut app = APP
.minimal()
.extend(FsWatcherManager::default())
.extend(ConfigManager::default())
.run_headless(false);
@ -171,7 +173,8 @@ mod tests {
#[test]
fn swap_config_swap_load() {
let mut app = App::minimal()
let mut app = APP
.minimal()
.extend(FsWatcherManager::default())
.extend(ConfigManager::default())
.run_headless(false);
@ -201,7 +204,8 @@ mod tests {
#[test]
fn swap_config_swap_load_delayed() {
let mut app = App::minimal()
let mut app = APP
.minimal()
.extend(FsWatcherManager::default())
.extend(ConfigManager::default())
.run_headless(false);
@ -234,7 +238,8 @@ mod tests {
#[test]
fn swap_config_swap_fallback_delayed() {
let mut app = App::minimal()
let mut app = APP
.minimal()
.extend(FsWatcherManager::default())
.extend(ConfigManager::default())
.run_headless(false);

View File

@ -1,6 +1,11 @@
//! Font features and variation types.
use std::{fmt, marker::PhantomData, num::NonZeroU32, collections::{HashMap, hash_map, HashSet}};
use std::{
collections::{hash_map, HashMap, HashSet},
fmt,
marker::PhantomData,
num::NonZeroU32,
};
use num_enum::FromPrimitive;

View File

@ -5,7 +5,6 @@ use hyphenation::{Hyphenator as _, Load as _};
use zero_ui_app_context::app_local;
use zero_ui_ext_l10n::Lang;
app_local! {
static HYPHENATION: Hyphenation = Hyphenation {
#[cfg(feature = "hyphenation_embed_all")]
@ -164,9 +163,9 @@ impl HyphenationDataSource for HyphenationDataEmbedded {
mod util {
use super::*;
use zero_ui_ext_l10n::{lang, Lang};
use hyphenation::Language::*;
use regex::Regex;
use zero_ui_ext_l10n::{lang, Lang};
app_local! {
pub static LANG_TO_LANGUAGE_MAP: Vec<(Lang, hyphenation::Language)> = vec![

View File

@ -3083,20 +3083,20 @@ impl CaretIndex {
#[cfg(test)]
mod tests {
use zero_ui_app::App;
use zero_ui_app::APP;
use super::*;
#[test]
fn generic_fonts_default() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
assert_eq!(FontName::sans_serif(), GenericFonts {}.sans_serif(&lang!(und)))
}
#[test]
fn generic_fonts_fallback() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
assert_eq!(FontName::sans_serif(), GenericFonts {}.sans_serif(&lang!(en_US)));
assert_eq!(FontName::sans_serif(), GenericFonts {}.sans_serif(&lang!(es)));
@ -3104,7 +3104,7 @@ mod tests {
#[test]
fn generic_fonts_get1() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(en_US), "Test Value");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "Test Value");
@ -3113,7 +3113,7 @@ mod tests {
#[test]
fn generic_fonts_get2() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(en), "Test Value");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "Test Value");
@ -3122,7 +3122,7 @@ mod tests {
#[test]
fn generic_fonts_get_best() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(en), "Test Value");
GenericFonts {}.set_sans_serif(lang!(en_US), "Best");
@ -3133,7 +3133,7 @@ mod tests {
#[test]
fn generic_fonts_get_no_lang_match() {
let _app = App::minimal().run_headless(false);
let _app = APP.minimal().run_headless(false);
GenericFonts {}.set_sans_serif(lang!(es_US), "Test Value");
assert_eq!(&GenericFonts {}.sans_serif(&lang!("en-US")), "sans-serif");

View File

@ -1,4 +1,4 @@
use std::{ops, collections::HashMap};
use std::{collections::HashMap, ops};
use crate::emoji_util;
@ -654,8 +654,7 @@ pub fn unicode_bidi_sort(
}
if !levels.is_empty() {
let (directions, vis_ranges) =
super::unicode_bidi_util::visual_runs(levels, line_classes, into_unic_level(base_direction));
let (directions, vis_ranges) = super::unicode_bidi_util::visual_runs(levels, line_classes, into_unic_level(base_direction));
for vis_range in vis_ranges {
if directions[vis_range.start].is_rtl() {
@ -708,10 +707,10 @@ fn into_unic_level(d: LayoutDirection) -> unicode_bidi::Level {
#[cfg(test)]
mod tests {
use zero_ui_layout::context::{TextSegmentKind, LayoutDirection};
use zero_ui_layout::context::{LayoutDirection, TextSegmentKind};
use zero_ui_txt::ToText;
use crate::{SegmentedText, TextSegment, BidiLevel};
use crate::{BidiLevel, SegmentedText, TextSegment};
#[test]
fn segments() {

View File

@ -3861,7 +3861,7 @@ fn into_harf_direction(d: LayoutDirection) -> harfbuzz_rs::Direction {
#[cfg(test)]
mod tests {
use crate::{Font, FontManager, FontName, FontStretch, FontStyle, FontWeight, SegmentedText, TextShapingArgs, WordContextKey, FONTS};
use zero_ui_app::App;
use zero_ui_app::APP;
use zero_ui_ext_l10n::lang;
use zero_ui_layout::{
context::LayoutDirection,
@ -3869,7 +3869,7 @@ mod tests {
};
fn test_font() -> Font {
let mut app = App::minimal().extend(FontManager::default()).run_headless(false);
let mut app = APP.minimal().extend(FontManager::default()).run_headless(false);
let font = app
.block_on_fut(
async {
@ -3976,7 +3976,7 @@ mod tests {
#[test]
fn font_fallback_issue() {
let mut app = App::minimal().extend(FontManager::default()).run_headless(false);
let mut app = APP.minimal().extend(FontManager::default()).run_headless(false);
app.block_on_fut(
async {
let font = FONTS

View File

@ -3,7 +3,7 @@
//! Text is only used in the source crate to handle character with multiple bytes, but we do bidi sorting at
//! the "segment" level.
use std::{ops::Range, collections::HashMap};
use std::{collections::HashMap, ops::Range};
use unicode_bidi::{BidiClass, BidiDataSource, Level};

View File

@ -30,7 +30,7 @@ mod types;
pub use types::*;
mod render;
pub use render::{render_retain, ImageRenderWindowRoot, ImageRenderWindowsService, IMAGE_RENDER};
pub use render::{render_retain, ImageRenderWindowRoot, ImageRenderWindowsService, IMAGES_WINDOW, IMAGE_RENDER};
use zero_ui_layout::units::{ByteLength, ByteUnits};
use zero_ui_task::ui::UiTask;
use zero_ui_txt::{formatx, ToText, Txt};

View File

@ -188,11 +188,16 @@ impl IMAGES {
{
IMAGES_SV.write().render_node(render_mode, scale_factor.into(), mask, render)
}
}
/// Images render window hook.
#[allow(non_camel_case_types)]
pub struct IMAGES_WINDOW;
impl IMAGES_WINDOW {
/// Sets the windows service used to manage the headless windows used to render images.
///
/// This must be called by the windows implementation only.
pub fn init_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
let mut img = IMAGES_SV.write();
assert!(img.render.windows.is_none());
img.render.windows = Some(service);
@ -371,7 +376,7 @@ pub trait ImageRenderWindowsService: Send + Sync + 'static {
/// Open the window.
///
/// The `new_window_root` is called inside the [`WINDOW`] context for the new window.
fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot>>);
fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
/// Returns the rendered frame image if it is ready for reading.
fn on_frame_image_ready(&self, update: &EventUpdate) -> Option<(WindowId, Img)>;
@ -385,10 +390,5 @@ pub trait ImageRenderWindowsService: Send + Sync + 'static {
/// This is implemented for the `WindowRoot` type.
pub trait ImageRenderWindowRoot: Send + Any + 'static {
/// Cast to `Box<dyn Any>`.
fn into_any(self: Box<Self>) -> Box<dyn Any>
where
Self: Sized,
{
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any>;
}

View File

@ -0,0 +1,25 @@
[package]
name = "zero-ui-ext-input"
version = "0.1.0"
authors = ["Samuel Guerra <sam.rodr.g@gmail.com>", "Well <well-r@hotmail.com>"]
edition = "2021"
license = "Apache-2.0"
[dependencies]
zero-ui-app_context = { path = "../zero-ui-app_context" }
zero-ui-clone_move = { path = "../zero-ui-clone_move" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-app = { path = "../zero-ui-app" }
zero-ui-txt = { path = "../zero-ui-txt" }
zero-ui-var = { path = "../zero-ui-var" }
zero-ui-layout = { path = "../zero-ui-layout" }
zero-ui-handle = { path = "../zero-ui-handle" }
zero-ui-state_map = { path = "../zero-ui-state_map" }
zero-ui-unique_id = { path = "../zero-ui-unique_id" }
zero-ui-ext-window = { path = "../zero-ui-ext-window" }
tracing = "0.1"
parking_lot = "0.12"
serde = { version = "1", features = ["derive"] }
bitflags = "2"
atomic = "0.6"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,229 @@
//! Commands that control focus and [`Command`] extensions.
use zero_ui_app::{
event::{command, Command, CommandHandle, CommandInfoExt, CommandNameExt, CommandScope, EventArgs},
shortcut::{shortcut, CommandShortcutExt},
update::EventUpdate,
widget::info::WidgetInfo,
};
use zero_ui_var::{merge_var, BoxedVar};
use super::*;
command! {
/// Represents the **focus next** action.
pub static FOCUS_NEXT_CMD = {
name: "Focus Next",
info: "Focus next focusable.",
shortcut: shortcut!(Tab),
};
/// Represents the **focus previous** action.
pub static FOCUS_PREV_CMD = {
name: "Focus Previous",
info: "Focus previous focusable.",
shortcut: shortcut!(SHIFT+Tab),
};
/// Represents the **focus/escape alt** action.
pub static FOCUS_ALT_CMD = {
name: "Focus Alt",
info: "Focus alt scope.",
shortcut: shortcut!(Alt),
};
/// Represents the **focus enter** action.
pub static FOCUS_ENTER_CMD = {
name: "Focus Enter",
info: "Focus child focusable.",
shortcut: [shortcut!(Enter), shortcut!(ALT+Enter)],
};
/// Represents the **focus exit** action.
pub static FOCUS_EXIT_CMD = {
name: "Focus Exit",
info: "Focus parent focusable, or return focus.",
shortcut: [shortcut!(Escape), shortcut!(ALT+Escape)],
};
/// Represents the **focus up** action.
pub static FOCUS_UP_CMD = {
name: "Focus Up",
info: "Focus closest focusable up.",
shortcut: [shortcut!(ArrowUp), shortcut!(ALT+ArrowUp)],
};
/// Represents the **focus down** action.
pub static FOCUS_DOWN_CMD = {
name: "Focus Down",
info: "Focus closest focusable down.",
shortcut: [shortcut!(ArrowDown), shortcut!(ALT+ArrowDown)],
};
/// Represents the **focus left** action.
pub static FOCUS_LEFT_CMD = {
name: "Focus Left",
info: "Focus closest focusable left.",
shortcut: [shortcut!(ArrowLeft), shortcut!(ALT+ArrowLeft)],
};
/// Represents the **focus right** action.
pub static FOCUS_RIGHT_CMD = {
name: "Focus Right",
info: "Focus closest focusable right.",
shortcut: [shortcut!(ArrowRight), shortcut!(ALT+ArrowRight)],
};
/// Represents a [`FocusRequest`] action.
///
/// If this command parameter is a [`FocusRequest`] the request is made.
pub static FOCUS_CMD;
}
pub(super) struct FocusCommands {
next_handle: CommandHandle,
prev_handle: CommandHandle,
alt_handle: CommandHandle,
up_handle: CommandHandle,
down_handle: CommandHandle,
left_handle: CommandHandle,
right_handle: CommandHandle,
exit_handle: CommandHandle,
enter_handle: CommandHandle,
#[allow(dead_code)]
focus_handle: CommandHandle,
}
impl FocusCommands {
pub fn new() -> Self {
Self {
next_handle: FOCUS_NEXT_CMD.subscribe(false),
prev_handle: FOCUS_PREV_CMD.subscribe(false),
alt_handle: FOCUS_ALT_CMD.subscribe(false),
up_handle: FOCUS_UP_CMD.subscribe(false),
down_handle: FOCUS_DOWN_CMD.subscribe(false),
left_handle: FOCUS_LEFT_CMD.subscribe(false),
right_handle: FOCUS_RIGHT_CMD.subscribe(false),
exit_handle: FOCUS_EXIT_CMD.subscribe(false),
enter_handle: FOCUS_ENTER_CMD.subscribe(false),
focus_handle: FOCUS_CMD.subscribe(true),
}
}
pub fn update_enabled(&mut self, nav: FocusNavAction) {
self.next_handle.set_enabled(nav.contains(FocusNavAction::NEXT));
self.prev_handle.set_enabled(nav.contains(FocusNavAction::PREV));
self.alt_handle.set_enabled(nav.contains(FocusNavAction::ALT));
self.up_handle.set_enabled(nav.contains(FocusNavAction::UP));
self.down_handle.set_enabled(nav.contains(FocusNavAction::DOWN));
self.left_handle.set_enabled(nav.contains(FocusNavAction::LEFT));
self.right_handle.set_enabled(nav.contains(FocusNavAction::RIGHT));
self.exit_handle.set_enabled(nav.contains(FocusNavAction::EXIT));
self.enter_handle.set_enabled(nav.contains(FocusNavAction::ENTER));
}
pub fn event_preview(&mut self, update: &EventUpdate) {
macro_rules! handle {
($($CMD:ident($handle:ident) => $method:ident,)+) => {$(
if let Some(args) = $CMD.on(update) {
args.handle(|args| {
if args.enabled && self.$handle.is_enabled() {
FOCUS.$method();
} else {
FOCUS.on_disabled_cmd();
}
});
return;
}
)+};
}
handle! {
FOCUS_NEXT_CMD(next_handle) => focus_next,
FOCUS_PREV_CMD(prev_handle) => focus_prev,
FOCUS_ALT_CMD(alt_handle) => focus_alt,
FOCUS_UP_CMD(up_handle) => focus_up,
FOCUS_DOWN_CMD(down_handle) => focus_down,
FOCUS_LEFT_CMD(left_handle) => focus_left,
FOCUS_RIGHT_CMD(right_handle) => focus_right,
FOCUS_ENTER_CMD(enter_handle) => focus_enter,
FOCUS_EXIT_CMD(exit_handle) => focus_exit,
}
if let Some(args) = FOCUS_CMD.on(update) {
if let Some(req) = args.param::<FocusRequest>() {
args.handle_enabled(&self.focus_handle, |_| {
FOCUS.focus(*req);
});
}
}
}
}
/// Focus extension methods for commands.
pub trait CommandFocusExt {
/// Gets a command variable with `self` scoped to the focused (non-alt) widget or app.
///
/// The scope is [`alt_return`] if is set, otherwise it is [`focused`], otherwise the
/// command is not scoped (app scope). This means that you can bind the command variable to
/// a menu or toolbar button inside an *alt-scope* without losing track of the intended target
/// of the command.
///
/// [`alt_return`]: FOCUS::alt_return
/// [`focused`]: FOCUS::focused
///
/// # Examples
///
/// ```
/// # macro_rules! _demo { () => {
/// let paste_in_focused_cmd = PASTE_CMD.focus_scoped();
/// let is_enabled = paste_in_focused_cmd.flat_map(|c| c.is_enabled());
/// paste_in_focused_cmd.get().notify();
/// # }};
/// ```
fn focus_scoped(self) -> BoxedVar<Command>;
/// Gets a command variable with `self` scoped to the output of `map`.
///
/// The `map` closure is called every time the non-alt focused widget changes, that is the [`alt_return`] or
/// the [`focused`]. The closure input is the [`WidgetInfo`] for the focused widget and the output must be
/// a [`CommandScope`] for the command.
///
/// [`alt_return`]: FOCUS::alt_return
/// [`focused`]: FOCUS::focused
fn focus_scoped_with(self, map: impl FnMut(Option<WidgetInfo>) -> CommandScope + Send + 'static) -> BoxedVar<Command>;
}
impl CommandFocusExt for Command {
fn focus_scoped(self) -> BoxedVar<Command> {
let cmd = self.scoped(CommandScope::App);
merge_var!(FOCUS.alt_return(), FOCUS.focused(), move |alt, f| {
match alt.as_ref().or(f.as_ref()) {
Some(p) => cmd.scoped(p.widget_id()),
None => cmd,
}
})
.boxed()
}
fn focus_scoped_with(self, mut map: impl FnMut(Option<WidgetInfo>) -> CommandScope + Send + 'static) -> BoxedVar<Command> {
let cmd = self.scoped(CommandScope::App);
merge_var!(FOCUS.alt_return(), FOCUS.focused(), |alt, f| {
match alt.as_ref().or(f.as_ref()) {
Some(p) => WINDOWS.widget_tree(p.window_id()).ok()?.get(p.widget_id()),
None => None,
}
})
.map(move |w| cmd.scoped(map(w.clone())))
.boxed()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,186 @@
//! Focusable info tree iterators.
//!
use zero_ui_app::widget::info::{
iter::{self as w_iter, TreeIterator},
WidgetInfo,
};
use super::*;
/// Filter-maps an iterator of [`WidgetInfo`] to [`WidgetFocusInfo`].
pub trait IterFocusableExt<I: Iterator<Item = WidgetInfo>> {
/// Returns an iterator of only the focusable widgets.
///
/// See the [`FOCUS.focus_disabled_widgets`] and [`FOCUS.focus_hidden_widgets`] config for more on the parameter.
///
/// [`FOCUS.focus_disabled_widgets`]: crate::focus::FOCUS::focus_disabled_widgets
/// [`FOCUS.focus_hidden_widgets`]: crate::focus::FOCUS::focus_hidden_widgets
fn focusable(self, focus_disabled_widgets: bool, focus_hidden_widgets: bool) -> IterFocusuable<I>;
}
impl<I> IterFocusableExt<I> for I
where
I: Iterator<Item = WidgetInfo>,
{
fn focusable(self, focus_disabled_widgets: bool, focus_hidden_widgets: bool) -> IterFocusuable<I> {
IterFocusuable {
iter: self,
mode: FocusMode::new(focus_disabled_widgets, focus_hidden_widgets),
}
}
}
/// Filter a widget info iterator to only focusable items.
///
/// Use [`IterFocusableExt::focusable`] to create.
pub struct IterFocusuable<I: Iterator<Item = WidgetInfo>> {
iter: I,
mode: FocusMode,
}
impl<I> Iterator for IterFocusuable<I>
where
I: Iterator<Item = WidgetInfo>,
{
type Item = WidgetFocusInfo;
fn next(&mut self) -> Option<Self::Item> {
for next in self.iter.by_ref() {
if let Some(next) = next.into_focusable(self.mode.contains(FocusMode::DISABLED), self.mode.contains(FocusMode::HIDDEN)) {
return Some(next);
}
}
None
}
}
impl<I> DoubleEndedIterator for IterFocusuable<I>
where
I: Iterator<Item = WidgetInfo> + DoubleEndedIterator,
{
fn next_back(&mut self) -> Option<Self::Item> {
while let Some(next) = self.iter.next_back() {
if let Some(next) = next.into_focusable(self.mode.contains(FocusMode::DISABLED), self.mode.contains(FocusMode::HIDDEN)) {
return Some(next);
}
}
None
}
}
/// Iterator over all focusable items in a branch of the widget tree.
///
/// This `struct` is created by the [`descendants`] and [`self_and_descendants`] methods on [`WidgetFocusInfo`].
/// See its documentation for more.
///
/// [`descendants`]: WidgetFocusInfo::descendants
/// [`self_and_descendants`]: WidgetFocusInfo::self_and_descendants
pub struct FocusTreeIter<I>
where
I: TreeIterator,
{
iter: I,
mode: FocusMode,
}
impl<I> FocusTreeIter<I>
where
I: TreeIterator,
{
pub(super) fn new(iter: I, mode: FocusMode) -> Self {
Self { iter, mode }
}
/// Filter out entire branches of descendants at a time.
///
/// Note that you can convert `bool` into [`TreeFilter`] to use this method just like the iterator default.
///
/// [`TreeFilter`]: w_iter::TreeFilter
pub fn tree_filter<F>(self, mut filter: F) -> FocusTreeFilterIter<I, impl FnMut(&WidgetInfo) -> w_iter::TreeFilter>
where
F: FnMut(&WidgetFocusInfo) -> w_iter::TreeFilter,
{
FocusTreeFilterIter {
iter: self.iter.tree_filter(move |w| {
if let Some(f) = w
.clone()
.into_focusable(self.mode.contains(FocusMode::DISABLED), self.mode.contains(FocusMode::HIDDEN))
{
filter(&f)
} else {
w_iter::TreeFilter::Skip
}
}),
mode: self.mode,
}
}
/// Returns the first focusable included by `filter`.
///
/// Note that you can convert `bool` into [`TreeFilter`] to use this method just like the iterator default.
///
/// [`TreeFilter`]: w_iter::TreeFilter
pub fn tree_find<F>(self, filter: F) -> Option<WidgetFocusInfo>
where
F: FnMut(&WidgetFocusInfo) -> w_iter::TreeFilter,
{
#[allow(clippy::filter_next)]
self.tree_filter(filter).next()
}
/// Returns if the `filter` allows any focusable.
///
/// Note that you can convert `bool` into [`TreeFilter`] to use this method just like the iterator default.
///
/// [`TreeFilter`]: w_iter::TreeFilter
pub fn tree_any<F>(self, filter: F) -> bool
where
F: FnMut(&WidgetFocusInfo) -> w_iter::TreeFilter,
{
self.tree_find(filter).is_some()
}
}
impl FocusTreeIter<w_iter::TreeIter> {
/// Creates a reverse tree iterator.
pub fn tree_rev(self) -> FocusTreeIter<w_iter::RevTreeIter> {
FocusTreeIter::new(self.iter.tree_rev(), self.mode)
}
}
impl<I> Iterator for FocusTreeIter<I>
where
I: TreeIterator,
{
type Item = WidgetFocusInfo;
fn next(&mut self) -> Option<Self::Item> {
for next in self.iter.by_ref() {
if let Some(next) = next.into_focusable(self.mode.contains(FocusMode::DISABLED), self.mode.contains(FocusMode::HIDDEN)) {
return Some(next);
}
}
None
}
}
/// An iterator that filters a focusable widget tree.
///
/// This `struct` is created by the [`FocusTreeIter::tree_filter`] method. See its documentation for more.
pub struct FocusTreeFilterIter<I, F>
where
I: TreeIterator,
F: FnMut(&WidgetInfo) -> w_iter::TreeFilter,
{
iter: w_iter::TreeFilterIter<I, F>,
mode: FocusMode,
}
impl<I, F> Iterator for FocusTreeFilterIter<I, F>
where
F: FnMut(&WidgetInfo) -> w_iter::TreeFilter,
I: TreeIterator,
{
type Item = WidgetFocusInfo;
fn next(&mut self) -> Option<Self::Item> {
self.iter
.next()
.map(|w| w.into_focus_info(self.mode.contains(FocusMode::DISABLED), self.mode.contains(FocusMode::HIDDEN)))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,547 @@
//! Keyboard manager.
use std::{
collections::HashSet,
time::{Duration, Instant},
};
use zero_ui_app::{
event::{event, event_args},
shortcut::ModifiersState,
update::EventUpdate,
view_process::{
raw_device_events::DeviceId,
raw_events::{
RawKeyInputArgs, RAW_ANIMATIONS_CONFIG_CHANGED_EVENT, RAW_KEY_INPUT_EVENT, RAW_KEY_REPEAT_CONFIG_CHANGED_EVENT,
RAW_WINDOW_FOCUS_EVENT,
},
VIEW_PROCESS_INITED_EVENT,
},
widget::{info::InteractionPath, WidgetId},
window::WindowId,
AppExtension, HeadlessApp,
};
use zero_ui_app_context::app_local;
use zero_ui_clone_move::clmv;
use zero_ui_layout::units::{Factor, FactorUnits};
use zero_ui_txt::Txt;
use zero_ui_var::{var, var_default, ArcVar, ReadOnlyArcVar, Var};
use zero_ui_view_api::config::AnimationsConfig;
pub use zero_ui_view_api::{
config::KeyRepeatConfig,
keyboard::{Key, KeyCode, KeyState, NativeKeyCode},
};
use crate::focus::FOCUS;
event_args! {
/// Arguments for [`KEY_INPUT_EVENT`].
pub struct KeyInputArgs {
/// Window that received the event.
pub window_id: WindowId,
/// Device that generated the event.
pub device_id: DeviceId,
/// Physical key.
pub key_code: KeyCode,
/// If the key was pressed or released.
pub state: KeyState,
/// Semantic key.
///
/// Pressing `Shift+A` key will produce `Key::Char('a')` in QWERT keyboards, the modifiers are not applied.
pub key: Key,
/// Semantic key modified by the current active modifiers.
///
/// Pressing `Shift+A` key will produce `Key::Char('A')` in QWERT keyboards, the modifiers are applied.
pub key_modified: Key,
/// Text typed.
///
/// This is only set during [`KeyState::Pressed`] of a key that generates text.
///
/// This is usually the `key_modified` char, but is also `'\r'` for `Key::Enter`. On Windows when a dead key was
/// pressed earlier but cannot be combined with the character from this key press, the produced text
/// will consist of two characters: the dead-key-character followed by the character resulting from this key press.
pub text: Txt,
/// What modifier keys where pressed when this event happened.
pub modifiers: ModifiersState,
/// Number of repeats generated by holding the key pressed.
///
/// This is zero for the first key press, increments by one for each event while the key is held pressed.
pub repeat_count: u32,
/// The focused element at the time of the key input.
pub target: InteractionPath,
..
/// The [`target`](Self::target).
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
list.insert_wgt(&self.target)
}
}
/// Arguments for [`MODIFIERS_CHANGED_EVENT`].
pub struct ModifiersChangedArgs {
/// Previous modifiers state.
pub prev_modifiers: ModifiersState,
/// Current modifiers state.
pub modifiers: ModifiersState,
..
/// Broadcast to all.
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
list.search_all()
}
}
}
impl KeyInputArgs {
/// Returns `true` if the widget is enabled in [`target`].
///
/// [`target`]: Self::target
pub fn is_enabled(&self, widget_id: WidgetId) -> bool {
self.target.interactivity_of(widget_id).map(|i| i.is_enabled()).unwrap_or(false)
}
/// Returns `true` if the widget is disabled in [`target`].
///
/// [`target`]: Self::target
pub fn is_disabled(&self, widget_id: WidgetId) -> bool {
self.target.interactivity_of(widget_id).map(|i| i.is_disabled()).unwrap_or(false)
}
}
/// Text input methods.
///
/// The [`text`] field contains the raw text associated with the key-press by the operating system,
/// these methods normalize and filter this text.
///
/// [`text`]: KeyInputArgs::text
impl KeyInputArgs {
/// Returns `true` if the character is the backspace and `CTRL` is not pressed.
pub fn is_backspace(&self) -> bool {
!self.modifiers.contains(ModifiersState::CTRL) && self.text.contains('\u{8}')
}
/// Returns `true` if the character is delete and `CTRL` is not pressed.
pub fn is_delete(&self) -> bool {
!self.modifiers.contains(ModifiersState::CTRL) && self.text.contains('\u{7F}')
}
/// Returns `true` if the character is a tab space and `CTRL` is not pressed.
pub fn is_tab(&self) -> bool {
!self.modifiers.contains(ModifiersState::CTRL) && self.text.chars().any(|c| "\t\u{B}\u{1F}".contains(c))
}
/// Returns `true` if the character is a line-break and `CTRL` is not pressed.
pub fn is_line_break(&self) -> bool {
!self.modifiers.contains(ModifiersState::CTRL) && self.text.chars().any(|c| "\r\n\u{85}".contains(c))
}
/// Gets the characters to insert in a typed text.
///
/// Replaces all [`is_tab`] with `\t` and all [`is_line_break`] with `\n`.
/// Returns `""` if there is no text or it contains ASCII control characters or `CTRL` is pressed.
///
/// [`is_tab`]: Self::is_tab
/// [`is_line_break`]: Self::is_line_break
pub fn insert_str(&self) -> &str {
if self.modifiers.contains(ModifiersState::CTRL) {
// ignore legacy ASCII control combinators like `ctrl+i` generated `\t`.
""
} else if self.is_tab() {
"\t"
} else if self.is_line_break() {
"\n"
} else if self.text.chars().any(|c| c.is_ascii_control()) {
""
} else {
&self.text
}
}
}
event! {
/// Key pressed, repeat pressed or released event.
///
/// # Provider
///
/// This event is provided by the [`KeyboardManager`] extension.
pub static KEY_INPUT_EVENT: KeyInputArgs;
/// Modifiers key state changed event.
///
/// # Provider
///
/// This event is provided by the [`KeyboardManager`] extension.
pub static MODIFIERS_CHANGED_EVENT: ModifiersChangedArgs;
}
/// Application extension that provides keyboard events targeting the focused widget.
///
/// This [extension] processes the raw keyboard events retargeting then to the focused widget, generating derived events and variables.
///
/// # Events
///
/// Events this extension provides.
///
/// * [`KEY_INPUT_EVENT`]
/// * [`MODIFIERS_CHANGED_EVENT`]
///
/// # Services
///
/// Services this extension provides.
///
/// * [`KEYBOARD`]
///
/// # Dependencies
///
/// This extension requires the [`FOCUS`] and [`WINDOWS`] services before the first raw key input event. It does not
/// require anything for initialization.
///
/// [extension]: AppExtension
/// [default app]: zero_ui_app::APP::default
/// [`FOCUS`]: crate::focus::FOCUS
/// [`WINDOWS`]: zero_ui_ext_window::WINDOWS
#[derive(Default)]
pub struct KeyboardManager {}
impl AppExtension for KeyboardManager {
fn event_preview(&mut self, update: &mut EventUpdate) {
if let Some(args) = RAW_KEY_INPUT_EVENT.on(update) {
let focused = FOCUS.focused().get();
KEYBOARD_SV.write().key_input(args, focused);
} else if let Some(args) = RAW_KEY_REPEAT_CONFIG_CHANGED_EVENT.on(update) {
let mut kb = KEYBOARD_SV.write();
kb.repeat_config.set(args.config);
kb.last_key_down = None;
} else if let Some(args) = RAW_ANIMATIONS_CONFIG_CHANGED_EVENT.on(update) {
let kb = KEYBOARD_SV.read();
kb.caret_animation_config
.set((args.config.caret_blink_interval, args.config.caret_blink_timeout));
} else if let Some(args) = RAW_WINDOW_FOCUS_EVENT.on(update) {
if args.new_focus.is_none() {
let mut kb = KEYBOARD_SV.write();
kb.clear_modifiers();
kb.codes.set(vec![]);
kb.keys.set(vec![]);
kb.last_key_down = None;
}
} else if let Some(args) = VIEW_PROCESS_INITED_EVENT.on(update) {
let mut kb = KEYBOARD_SV.write();
kb.repeat_config.set(args.key_repeat_config);
kb.caret_animation_config.set((
args.animations_config.caret_blink_interval,
args.animations_config.caret_blink_timeout,
));
if args.is_respawn {
kb.clear_modifiers();
kb.codes.set(vec![]);
kb.keys.set(vec![]);
kb.last_key_down = None;
}
}
}
}
/// Keyboard service.
///
/// # Provider
///
/// This service is provided by the [`KeyboardManager`] extension.
pub struct KEYBOARD;
impl KEYBOARD {
/// Returns a read-only variable that tracks the currently pressed modifier keys.
pub fn modifiers(&self) -> ReadOnlyArcVar<ModifiersState> {
KEYBOARD_SV.read().modifiers.read_only()
}
/// Returns a read-only variable that tracks the [`KeyCode`] of the keys currently pressed.
pub fn codes(&self) -> ReadOnlyArcVar<Vec<KeyCode>> {
KEYBOARD_SV.read().codes.read_only()
}
/// Returns a read-only variable that tracks the [`Key`] identifier of the keys currently pressed.
pub fn keys(&self) -> ReadOnlyArcVar<Vec<Key>> {
KEYBOARD_SV.read().keys.read_only()
}
/// Returns a read-only variable that tracks the operating system key press repeat start delay and repeat speed.
///
/// This delay is roughly the time the user must hold a key pressed to start repeating. When a second key press
/// happens without any other keyboard event and within twice this value it increments the [`repeat_count`] by the [`KeyboardManager`].
///
/// [`repeat_count`]: KeyInputArgs::repeat_count
/// [`repeat_speed`]: Self::repeat_speed
pub fn repeat_config(&self) -> ReadOnlyArcVar<KeyRepeatConfig> {
KEYBOARD_SV.read().repeat_config.read_only()
}
/// Returns a read-only variable that defines the system config for the caret blink speed and timeout.
///
/// The first value defines the blink speed interval, the caret is visible for the duration, then not visible for the duration. The
/// second value defines the animation total duration, the caret stops animating and sticks to visible after this timeout is reached.
///
/// You can use the [`caret_animation`] method to generate a new animation.
///
/// [`caret_animation`]: Self::caret_animation
pub fn caret_animation_config(&self) -> ReadOnlyArcVar<(Duration, Duration)> {
KEYBOARD_SV.read().caret_animation_config.read_only()
}
/// Returns a new read-only variable that animates the caret opacity.
///
/// A new animation must be started after each key press. The value is always 1 or 0, no easing is used by default,
/// it can be added using the [`Var::easing`] method.
pub fn caret_animation(&self) -> ReadOnlyArcVar<Factor> {
KEYBOARD_SV.read().caret_animation()
}
}
app_local! {
static KEYBOARD_SV: KeyboardService = KeyboardService {
current_modifiers: HashSet::default(),
modifiers: var(ModifiersState::empty()),
codes: var(vec![]),
keys: var(vec![]),
repeat_config: var_default(),
caret_animation_config: {
let cfg = AnimationsConfig::default();
var((cfg.caret_blink_interval, cfg.caret_blink_timeout))
},
last_key_down: None,
};
}
struct KeyboardService {
current_modifiers: HashSet<Key>,
modifiers: ArcVar<ModifiersState>,
codes: ArcVar<Vec<KeyCode>>,
keys: ArcVar<Vec<Key>>,
repeat_config: ArcVar<KeyRepeatConfig>,
caret_animation_config: ArcVar<(Duration, Duration)>,
last_key_down: Option<(DeviceId, KeyCode, Instant, u32)>,
}
impl KeyboardService {
fn key_input(&mut self, args: &RawKeyInputArgs, focused: Option<InteractionPath>) {
let mut repeat = 0;
// update state and vars
match args.state {
KeyState::Pressed => {
if let Some((d_id, code, time, count)) = &mut self.last_key_down {
let max_t = self.repeat_config.get().start_delay * 2;
if args.key_code == *code && args.device_id == *d_id && (args.timestamp - *time) < max_t {
*count = (*count).saturating_add(1);
repeat = *count;
} else {
*d_id = args.device_id;
*code = args.key_code;
*count = 0;
}
*time = args.timestamp;
} else {
self.last_key_down = Some((args.device_id, args.key_code, args.timestamp, 0));
}
let key_code = args.key_code;
if !self.codes.with(|c| c.contains(&key_code)) {
self.codes.modify(move |cs| {
cs.to_mut().push(key_code);
});
}
let key = &args.key;
if !matches!(&key, Key::Unidentified) {
if !self.keys.with(|c| c.contains(key)) {
self.keys.modify(clmv!(key, |ks| {
ks.to_mut().push(key);
}));
}
if key.is_modifier() {
self.set_modifiers(key.clone(), true);
}
}
}
KeyState::Released => {
self.last_key_down = None;
let key = args.key_code;
if self.codes.with(|c| c.contains(&key)) {
self.codes.modify(move |cs| {
if let Some(i) = cs.as_ref().iter().position(|c| *c == key) {
cs.to_mut().swap_remove(i);
}
});
}
let key = &args.key;
if !matches!(&key, Key::Unidentified) {
if self.keys.with(|c| c.contains(key)) {
self.keys.modify(clmv!(key, |ks| {
if let Some(i) = ks.as_ref().iter().position(|k| k == &key) {
ks.to_mut().swap_remove(i);
}
}));
}
if key.is_modifier() {
self.set_modifiers(key.clone(), false);
}
}
}
}
// notify events
if let Some(target) = focused {
if target.window_id() == args.window_id {
let args = KeyInputArgs::now(
args.window_id,
args.device_id,
args.key_code,
args.state,
args.key.clone(),
args.key_modified.clone(),
args.text.clone(),
self.current_modifiers(),
repeat,
target,
);
KEY_INPUT_EVENT.notify(args);
}
}
}
fn set_modifiers(&mut self, key: Key, pressed: bool) {
let prev_modifiers = self.current_modifiers();
if pressed {
self.current_modifiers.insert(key);
} else {
self.current_modifiers.remove(&key);
}
let new_modifiers = self.current_modifiers();
if prev_modifiers != new_modifiers {
self.modifiers.set(new_modifiers);
MODIFIERS_CHANGED_EVENT.notify(ModifiersChangedArgs::now(prev_modifiers, new_modifiers));
}
}
fn clear_modifiers(&mut self) {
let prev_modifiers = self.current_modifiers();
self.current_modifiers.clear();
let new_modifiers = self.current_modifiers();
if prev_modifiers != new_modifiers {
self.modifiers.set(new_modifiers);
MODIFIERS_CHANGED_EVENT.notify(ModifiersChangedArgs::now(prev_modifiers, new_modifiers));
}
}
fn current_modifiers(&self) -> ModifiersState {
let mut state = ModifiersState::empty();
for key in &self.current_modifiers {
state |= ModifiersState::from_key(key.clone());
}
state
}
fn caret_animation(&self) -> ReadOnlyArcVar<Factor> {
let var = var(1.fct());
let cfg = self.caret_animation_config.clone();
let zero = 0.fct();
let one = 1.fct();
let mut init = true;
var.animate(move |anim, vm| {
let (interval, timeout) = cfg.get();
if anim.start_time().elapsed() >= timeout {
if **vm != one {
vm.set(one);
}
anim.stop();
} else {
if **vm == one {
if !std::mem::take(&mut init) {
vm.set(zero);
}
} else {
vm.set(one);
}
anim.sleep(interval);
}
})
.perm();
var.read_only()
}
}
/// Extension trait that adds keyboard simulation methods to [`HeadlessApp`].
pub trait HeadlessAppKeyboardExt {
/// Notifies keyboard input event.
///
/// Note that the app is not updated so the event is pending after this call.
fn on_keyboard_input(&mut self, window_id: WindowId, code: KeyCode, key: Key, state: KeyState);
/// Does a key-down, key-up and updates.
fn press_key(&mut self, window_id: WindowId, code: KeyCode, key: Key);
/// Does a modifiers changed, key-down, key-up, reset modifiers and updates.
fn press_modified_key(&mut self, window_id: WindowId, modifiers: ModifiersState, code: KeyCode, key: Key);
}
impl HeadlessAppKeyboardExt for HeadlessApp {
fn on_keyboard_input(&mut self, window_id: WindowId, code: KeyCode, key: Key, state: KeyState) {
use zero_ui_app::view_process::raw_events::*;
let args = RawKeyInputArgs::now(window_id, DeviceId::virtual_keyboard(), code, state, key.clone(), key, "");
RAW_KEY_INPUT_EVENT.notify(args);
}
fn press_key(&mut self, window_id: WindowId, code: KeyCode, key: Key) {
self.on_keyboard_input(window_id, code, key.clone(), KeyState::Pressed);
self.on_keyboard_input(window_id, code, key, KeyState::Released);
let _ = self.update(false);
}
fn press_modified_key(&mut self, window_id: WindowId, modifiers: ModifiersState, code: KeyCode, key: Key) {
if modifiers.is_empty() {
self.press_key(window_id, code, key);
} else {
let modifiers = modifiers.keys();
for key in &modifiers {
self.on_keyboard_input(window_id, code, key.clone(), KeyState::Pressed);
}
// pressed the modifiers.
let _ = self.update(false);
self.on_keyboard_input(window_id, code, key.clone(), KeyState::Pressed);
self.on_keyboard_input(window_id, code, key.clone(), KeyState::Released);
// pressed the key.
let _ = self.update(false);
for key in modifiers {
self.on_keyboard_input(window_id, code, key, KeyState::Released);
}
// released the modifiers.
let _ = self.update(false);
}
}
}

View File

@ -0,0 +1,11 @@
//! Input events and focused widget.
#[macro_use]
extern crate bitflags;
pub mod focus;
pub mod gesture;
pub mod keyboard;
pub mod mouse;
pub mod pointer_capture;
pub mod touch;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,460 @@
//! Mouse and touch capture.
use std::{
collections::{HashMap, HashSet},
fmt, mem,
};
use zero_ui_app::{
event::{event, event_args},
update::{EventUpdate, UPDATES},
view_process::{
raw_device_events::DeviceId,
raw_events::{
RAW_FRAME_RENDERED_EVENT, RAW_MOUSE_INPUT_EVENT, RAW_MOUSE_MOVED_EVENT, RAW_TOUCH_EVENT, RAW_WINDOW_CLOSE_EVENT,
RAW_WINDOW_FOCUS_EVENT,
},
VIEW_PROCESS_INITED_EVENT,
},
widget::{
info::{InteractionPath, WidgetInfoTree, WidgetPath, WIDGET_INFO_CHANGED_EVENT},
WidgetId, WIDGET,
},
window::{WindowId, WINDOW},
AppExtension,
};
use zero_ui_app_context::app_local;
use zero_ui_ext_window::WINDOWS;
use zero_ui_layout::units::{DipPoint, DipToPx};
use zero_ui_var::{impl_from_and_into_var, var, ArcVar, ReadOnlyArcVar, Var};
use zero_ui_view_api::{
mouse::{ButtonState, MouseButton},
touch::{TouchId, TouchPhase},
};
/// Application extension that provides mouse and touch capture service.
///
/// # Events
///
/// Events this extension provides.
///
/// * [`POINTER_CAPTURE_EVENT`]
///
/// # Services
///
/// Services this extension provides.
///
/// * [`POINTER_CAPTURE`]
#[derive(Default)]
pub struct PointerCaptureManager {
mouse_position: HashMap<(WindowId, DeviceId), DipPoint>,
mouse_down: HashSet<(WindowId, DeviceId, MouseButton)>,
touch_down: HashSet<(WindowId, DeviceId, TouchId)>,
capture: Option<CaptureInfo>,
}
impl AppExtension for PointerCaptureManager {
fn event(&mut self, update: &mut EventUpdate) {
if let Some(args) = RAW_FRAME_RENDERED_EVENT.on(update) {
if let Some(c) = &self.capture {
if c.target.window_id() == args.window_id {
if let Ok(info) = WINDOWS.widget_tree(args.window_id) {
self.continue_capture(&info);
}
// else will receive close event.
}
}
} else if let Some(args) = RAW_MOUSE_MOVED_EVENT.on(update) {
self.mouse_position.insert((args.window_id, args.device_id), args.position);
} else if let Some(args) = RAW_MOUSE_INPUT_EVENT.on(update) {
match args.state {
ButtonState::Pressed => {
if self.mouse_down.insert((args.window_id, args.device_id, args.button))
&& self.mouse_down.len() == 1
&& self.touch_down.is_empty()
{
self.on_first_down(
args.window_id,
self.mouse_position
.get(&(args.window_id, args.device_id))
.copied()
.unwrap_or_default(),
);
}
}
ButtonState::Released => {
if self.mouse_down.remove(&(args.window_id, args.device_id, args.button))
&& self.mouse_down.is_empty()
&& self.touch_down.is_empty()
{
self.on_last_up();
}
}
}
} else if let Some(args) = RAW_TOUCH_EVENT.on(update) {
for touch in &args.touches {
match touch.phase {
TouchPhase::Start => {
if self.touch_down.insert((args.window_id, args.device_id, touch.touch))
&& self.touch_down.len() == 1
&& self.mouse_down.is_empty()
{
self.on_first_down(args.window_id, touch.position);
}
}
TouchPhase::End | TouchPhase::Cancel => {
if self.touch_down.remove(&(args.window_id, args.device_id, touch.touch))
&& self.touch_down.is_empty()
&& self.mouse_down.is_empty()
{
self.on_last_up();
}
}
TouchPhase::Move => {}
}
}
} else if let Some(args) = WIDGET_INFO_CHANGED_EVENT.on(update) {
if let Some(c) = &self.capture {
if c.target.window_id() == args.window_id {
self.continue_capture(&args.tree);
}
}
} else if let Some(args) = RAW_WINDOW_CLOSE_EVENT.on(update) {
self.remove_window(args.window_id);
} else if let Some(args) = RAW_WINDOW_FOCUS_EVENT.on(update) {
if let Some(w) = args.prev_focus {
self.remove_window(w);
}
} else if let Some(args) = VIEW_PROCESS_INITED_EVENT.on(update) {
if args.is_respawn && (!self.mouse_down.is_empty() || !self.touch_down.is_empty()) {
self.mouse_down.clear();
self.touch_down.clear();
self.on_last_up();
}
}
}
fn update(&mut self) {
if let Some(current) = &self.capture {
let mut cap = POINTER_CAPTURE_SV.write();
if let Some((widget_id, mode)) = cap.capture_request.take() {
if let Ok(true) = WINDOWS.is_focused(current.target.window_id()) {
// current window pressed
if let Some(widget) = WINDOWS.widget_tree(current.target.window_id()).unwrap().get(widget_id) {
// request valid
self.set_capture(&mut cap, widget.interaction_path(), mode);
}
}
} else if mem::take(&mut cap.release_requested) && current.mode != CaptureMode::Window {
// release capture (back to default capture).
let target = current.target.root_path();
self.set_capture(&mut cap, InteractionPath::from_enabled(target.into_owned()), CaptureMode::Window);
}
}
}
}
impl PointerCaptureManager {
fn remove_window(&mut self, window_id: WindowId) {
self.mouse_position.retain(|(w, _), _| *w != window_id);
if !self.mouse_down.is_empty() || !self.touch_down.is_empty() {
self.mouse_down.retain(|(w, _, _)| *w != window_id);
self.touch_down.retain(|(w, _, _)| *w != window_id);
if self.mouse_down.is_empty() && self.touch_down.is_empty() {
self.on_last_up();
}
}
}
fn on_first_down(&mut self, window_id: WindowId, point: DipPoint) {
if let Ok(info) = WINDOWS.widget_tree(window_id) {
let mut cap = POINTER_CAPTURE_SV.write();
cap.release_requested = false;
if let Some((widget_id, mode)) = cap.capture_request.take() {
if let Some(w_info) = info.get(widget_id) {
let point = point.to_px(info.scale_factor());
if w_info.hit_test(point).contains(widget_id) {
// capture for widget
self.set_capture(&mut cap, w_info.interaction_path(), mode);
return;
}
}
}
// default capture
self.set_capture(&mut cap, info.root().interaction_path(), CaptureMode::Window);
}
}
fn on_last_up(&mut self) {
let mut cap = POINTER_CAPTURE_SV.write();
cap.release_requested = false;
cap.capture_request = None;
self.unset_capture(&mut cap);
}
fn continue_capture(&mut self, info: &WidgetInfoTree) {
let current = self.capture.as_ref().unwrap();
if let Some(widget) = info.get(current.target.widget_id()) {
if let Some(new_path) = widget.new_interaction_path(&InteractionPath::from_enabled(current.target.clone())) {
// widget moved inside window tree.
let mode = current.mode;
self.set_capture(&mut POINTER_CAPTURE_SV.write(), new_path, mode);
}
} else {
// widget not found. Returns to default capture.
self.set_capture(&mut POINTER_CAPTURE_SV.write(), info.root().interaction_path(), CaptureMode::Window);
}
}
fn set_capture(&mut self, cap: &mut PointerCaptureService, target: InteractionPath, mode: CaptureMode) {
let new = target.enabled().map(|target| CaptureInfo { target, mode });
if new.is_none() {
self.unset_capture(cap);
return;
}
if new != self.capture {
let prev = self.capture.take();
self.capture = new.clone();
cap.capture_value = new.clone();
cap.capture.set(new.clone());
POINTER_CAPTURE_EVENT.notify(PointerCaptureArgs::now(prev, new));
}
}
fn unset_capture(&mut self, cap: &mut PointerCaptureService) {
if self.capture.is_some() {
let prev = self.capture.take();
cap.capture_value = None;
cap.capture.set(None);
POINTER_CAPTURE_EVENT.notify(PointerCaptureArgs::now(prev, None));
}
}
}
/// Mouse and touch capture service.
///
/// Mouse and touch is **captured** when mouse and touch events are redirected to a specific target. The user
/// can still move the cursor or touch contact outside of the target but the widgets outside do not react to this.
///
/// You can request capture by calling [`capture_widget`](POINTER_CAPTURE::capture_widget) or
/// [`capture_subtree`](POINTER_CAPTURE::capture_subtree) with a widget that was pressed by a mouse button or by touch.
/// The capture will last for as long as any of the mouse buttons or touch contacts are pressed, the widget is visible
/// and the window is focused.
///
/// Windows capture by default, this cannot be disabled. For other widgets this is optional.
///
/// # Provider
///
/// This service is provided by the [`PointerCaptureManager`] extension.
#[allow(non_camel_case_types)]
pub struct POINTER_CAPTURE;
impl POINTER_CAPTURE {
/// Variable that gets the current capture target and mode.
pub fn current_capture(&self) -> ReadOnlyArcVar<Option<CaptureInfo>> {
POINTER_CAPTURE_SV.read().capture.read_only()
}
/// Set a widget to redirect all mouse and touch events to.
///
/// The capture will be set only if the widget is pressed.
pub fn capture_widget(&self, widget_id: WidgetId) {
let mut m = POINTER_CAPTURE_SV.write();
m.capture_request = Some((widget_id, CaptureMode::Widget));
UPDATES.update(None);
}
/// Set a widget to be the root of a capture subtree.
///
/// Mouse and touch events targeting inside the subtree go to target normally. Mouse and touch events outside
/// the capture root are redirected to the capture root.
///
/// The capture will be set only if the widget is pressed.
pub fn capture_subtree(&self, widget_id: WidgetId) {
let mut m = POINTER_CAPTURE_SV.write();
m.capture_request = Some((widget_id, CaptureMode::Subtree));
UPDATES.update(None);
}
/// Release the current mouse and touch capture back to window.
///
/// **Note:** The capture is released automatically when the mouse buttons or touch are released
/// or when the window loses focus.
pub fn release_capture(&self) {
let mut m = POINTER_CAPTURE_SV.write();
m.release_requested = true;
UPDATES.update(None);
}
/// Latest capture, already valid for the current raw mouse or touch event cycle.
pub(crate) fn current_capture_value(&self) -> Option<CaptureInfo> {
POINTER_CAPTURE_SV.read().capture_value.clone()
}
}
/// Mouse and touch capture mode.
#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum CaptureMode {
/// Mouse and touch captured by the window only.
///
/// Default behavior.
Window,
/// Mouse and touch events inside the widget sub-tree permitted. Mouse events
/// outside of the widget redirected to the widget.
Subtree,
/// Mouse and touch events redirected to the widget.
Widget,
}
impl fmt::Debug for CaptureMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "CaptureMode::")?;
}
match self {
CaptureMode::Window => write!(f, "Window"),
CaptureMode::Subtree => write!(f, "Subtree"),
CaptureMode::Widget => write!(f, "Widget"),
}
}
}
impl Default for CaptureMode {
/// [`CaptureMode::Window`]
fn default() -> Self {
CaptureMode::Window
}
}
impl_from_and_into_var! {
/// Convert `true` to [`CaptureMode::Widget`] and `false` to [`CaptureMode::Window`].
fn from(widget: bool) -> CaptureMode {
if widget {
CaptureMode::Widget
} else {
CaptureMode::Window
}
}
}
/// Information about mouse and touch capture in a mouse or touch event argument.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CaptureInfo {
/// Widget that is capturing all mouse and touch events. The widget and all ancestors are [`ENABLED`].
///
/// This is the window root widget for capture mode `Window`.
///
/// [`ENABLED`]: zero_ui_app::widget::info::Interactivity::ENABLED
pub target: WidgetPath,
/// Capture mode, see [`allows`](Self::allows) for more details.
pub mode: CaptureMode,
}
impl CaptureInfo {
/// If the widget is allowed by the current capture.
///
/// This method uses [`WINDOW`] and [`WIDGET`] to identify the widget context.
///
/// | Mode | Allows |
/// |----------------|----------------------------------------------------|
/// | `Window` | All widgets in the same window. |
/// | `Subtree` | All widgets that have the `target` in their path. |
/// | `Widget` | Only the `target` widget. |
pub fn allows(&self) -> bool {
match self.mode {
CaptureMode::Window => self.target.window_id() == WINDOW.id(),
CaptureMode::Widget => self.target.widget_id() == WIDGET.id(),
CaptureMode::Subtree => {
let tree = WINDOW.info();
if let Some(wgt) = tree.get(WIDGET.id()) {
for wgt in wgt.self_and_ancestors() {
if wgt.id() == self.target.widget_id() {
return true;
}
}
}
false
}
}
}
}
app_local! {
static POINTER_CAPTURE_SV: PointerCaptureService = PointerCaptureService {
capture_value: None,
capture: var(None),
capture_request: None,
release_requested: false,
};
}
struct PointerCaptureService {
capture_value: Option<CaptureInfo>,
capture: ArcVar<Option<CaptureInfo>>,
capture_request: Option<(WidgetId, CaptureMode)>,
release_requested: bool,
}
event! {
/// Mouse and touch capture changed event.
pub static POINTER_CAPTURE_EVENT: PointerCaptureArgs;
}
event_args! {
/// [`POINTER_CAPTURE_EVENT`] arguments.
pub struct PointerCaptureArgs {
/// Previous mouse and touch capture target and mode.
pub prev_capture: Option<CaptureInfo>,
/// new mouse and capture target and mode.
pub new_capture: Option<CaptureInfo>,
..
/// The [`prev_capture`] and [`new_capture`] paths start with the current path.
///
/// [`prev_capture`]: Self::prev_capture
/// [`new_capture`]: Self::new_capture
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
if let Some(p) = &self.prev_capture {
list.insert_wgt(&p.target);
}
if let Some(p) = &self.new_capture {
list.insert_wgt(&p.target);
}
}
}
}
impl PointerCaptureArgs {
/// If the same widget has pointer capture, but the widget path changed.
pub fn is_widget_move(&self) -> bool {
match (&self.prev_capture, &self.new_capture) {
(Some(prev), Some(new)) => prev.target.widget_id() == new.target.widget_id() && prev.target != new.target,
_ => false,
}
}
/// If the same widget has pointer capture, but the capture mode changed.
pub fn is_mode_change(&self) -> bool {
match (&self.prev_capture, &self.new_capture) {
(Some(prev), Some(new)) => prev.target.widget_id() == new.target.widget_id() && prev.mode != new.mode,
_ => false,
}
}
/// If the `widget_id` lost pointer capture with this update.
pub fn is_lost(&self, widget_id: WidgetId) -> bool {
match (&self.prev_capture, &self.new_capture) {
(None, _) => false,
(Some(p), None) => p.target.widget_id() == widget_id,
(Some(prev), Some(new)) => prev.target.widget_id() == widget_id && new.target.widget_id() != widget_id,
}
}
/// If the `widget_id` got pointer capture with this update.
pub fn is_got(&self, widget_id: WidgetId) -> bool {
match (&self.prev_capture, &self.new_capture) {
(_, None) => false,
(None, Some(p)) => p.target.widget_id() == widget_id,
(Some(prev), Some(new)) => prev.target.widget_id() != widget_id && new.target.widget_id() == widget_id,
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,7 @@ impl AppExtension for L10nManager {
/// ```
/// # use zero_ui_ext_l10n::*;
/// # use zero_ui_var::*;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
/// let name = var("World");
/// let msg = l10n!("file/id.attribute", "Hello {$name}!");
/// ```
@ -115,7 +115,7 @@ impl AppExtension for L10nManager {
/// ```
/// # use zero_ui_ext_l10n::*;
/// # use zero_ui_var::*;
/// # let _scope = zero_ui_app::App::minimal();
/// # let _scope = zero_ui_app::APP.minimal();
///
/// // l10n-### Standalone Note
///

View File

@ -12,6 +12,7 @@ zero-ui-app = { path = "../zero-ui-app" }
zero-ui-var = { path = "../zero-ui-var" }
zero-ui-txt = { path = "../zero-ui-txt" }
zero-ui-state_map = { path = "../zero-ui-state_map" }
zero-ui-ext-input = { path = "../zero-ui-ext-input" }
atomic = "0.6"
parking_lot = "0.12"

View File

@ -1,5 +1,8 @@
//! Undo-redo app extension, service and commands.
//!
#![recursion_limit = "256"]
// suppress nag about very simple boxed closure signatures.
#![allow(clippy::type_complexity)]
use std::{
any::Any,
@ -26,6 +29,7 @@ use zero_ui_app::{
};
use zero_ui_app_context::{app_local, context_local, RunOnDrop};
use zero_ui_clone_move::clmv;
use zero_ui_ext_input::{focus::commands::CommandFocusExt, keyboard::KEYBOARD};
use zero_ui_state_map::{StateMapRef, StaticStateId};
use zero_ui_txt::Txt;
use zero_ui_var::{context_var, var, BoxedVar, Var, VarHandle, VarValue, WeakVar};
@ -115,7 +119,7 @@ impl UNDO {
///
/// [`undo`]: Self::undo
/// [`redo`]: Self::redo
/// [keyboard repeat start delay + interval]: crate::keyboard::KEYBOARD::repeat_config
/// [keyboard repeat start delay + interval]: zero_ui_ext_input::keyboard::KEYBOARD::repeat_config
pub fn undo_interval(&self) -> BoxedVar<Duration> {
UNDO_SV.read().undo_interval.clone()
}
@ -1333,13 +1337,13 @@ impl UndoSelect for UndoSelectLtEq {
#[cfg(test)]
mod tests {
use zero_ui_app::App;
use zero_ui_app::APP;
use super::*;
#[test]
fn register() {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![1, 2]));
UNDO.register(PushAction {
@ -1382,7 +1386,7 @@ mod tests {
#[test]
fn run_op() {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![]));
push_1_2(&data);
@ -1401,7 +1405,7 @@ mod tests {
#[test]
fn transaction_undo() {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![]));
let t = UNDO.transaction(|| {
@ -1418,7 +1422,7 @@ mod tests {
#[test]
fn transaction_commit() {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![]));
let t = UNDO.transaction(|| {
@ -1444,7 +1448,7 @@ mod tests {
#[test]
fn transaction_group() {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![]));
let t = UNDO.transaction(|| {
@ -1484,7 +1488,7 @@ mod tests {
#[test]
fn undo_redo_t_zero() {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![]));
push_1_sleep_2(&data);
@ -1512,7 +1516,7 @@ mod tests {
}
fn undo_redo_t_large(t: Duration) {
let _a = App::minimal();
let _a = APP.minimal();
let data = Arc::new(Mutex::new(vec![]));
push_1_sleep_2(&data);
@ -1527,7 +1531,7 @@ mod tests {
#[test]
fn watch_var() {
let mut app = App::minimal().run_headless(false);
let mut app = APP.minimal().run_headless(false);
let test_var = var(0);
UNDO.watch_var("set test var", test_var.clone()).perm();

View File

@ -16,7 +16,6 @@ zero-ui-layout = { path = "../zero-ui-layout" }
zero-ui-state_map = { path = "../zero-ui-state_map" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-task = { path = "../zero-ui-task" }
zero-ui-color = { path = "../zero-ui-color" }
zero-ui-ext-image = { path = "../zero-ui-ext-image" }
serde = { version = "1", features = ["derive"] }

View File

@ -1,6 +1,12 @@
//! Commands that control the scoped window.
use zero_ui_app::{event::{command, CommandHandle}, window::{WindowId, WINDOW}, update::EventUpdate};
use zero_ui_app::{
event::{command, CommandHandle, CommandInfoExt, CommandNameExt},
shortcut::{shortcut, CommandShortcutExt},
update::EventUpdate,
window::{WindowId, WINDOW},
};
use zero_ui_var::Var;
use zero_ui_view_api::window::WindowState;
use crate::{WindowVars, WINDOWS};

View File

@ -5,7 +5,7 @@ use std::{mem, sync::Arc};
use zero_ui_app::{
access::ACCESS_INITED_EVENT,
app_hn_once,
event::CommandHandle,
event::{AnyEventArgs, CommandHandle},
render::{FrameBuilder, FrameUpdate},
timer::TIMERS,
update::{EventUpdate, InfoUpdates, LayoutUpdates, RenderUpdates, WidgetUpdates, UPDATES},
@ -18,22 +18,23 @@ use zero_ui_app::{
},
widget::{
info::{access::AccessEnabled, WidgetInfoBuilder, WidgetInfoTree, WidgetLayout, WidgetPath},
instance::BoxedUiNode,
WidgetCtx, WidgetId, WidgetUpdateMode, WIDGET,
instance::{BoxedUiNode, UiNode},
VarLayout, WidgetCtx, WidgetId, WidgetUpdateMode, WIDGET,
},
window::{WindowId, WindowMode, WINDOW},
};
use zero_ui_clone_move::clmv;
use zero_ui_color::RenderColor;
use zero_ui_ext_image::{ImageRenderArgs, ImageSource, ImageVar, Img, IMAGES};
use zero_ui_layout::{
context::{LayoutMetrics, LayoutPassId, DIRECTION_VAR, LAYOUT},
units::{Deadline, Dip, DipRect, DipSize, Factor, Length, Ppi, Px, PxPoint, PxRect, PxSize, PxTransform, PxVector},
units::{
Deadline, Dip, DipRect, DipSize, DipToPx, Factor, FactorUnits, Layout1d, Layout2d, Length, Ppi, Px, PxPoint, PxRect, PxSize,
PxToDip, PxVector, TimeUnits,
},
};
use zero_ui_unique_id::IdMap;
use zero_ui_var::{ReadOnlyArcVar, VarHandle, VarHandles};
use zero_ui_var::{AnyVar, ReadOnlyArcVar, Var, VarHandle, VarHandles};
use zero_ui_view_api::{
config::ColorScheme,
config::{ColorScheme, FontAntiAliasing},
window::{
EventCause, FrameCapture, FrameId, FrameRequest, FrameUpdateRequest, FrameWaitId, HeadlessRequest, RenderMode, WindowRequest,
WindowState, WindowStateAll,
@ -43,9 +44,9 @@ use zero_ui_view_api::{
use crate::{
commands::{WindowCommands, MINIMIZE_CMD, RESTORE_CMD},
AutoSize, CursorImage, FrameCaptureMode, FrameImageReadyArgs, HeadlessMonitor, MonitorInfo, StartPosition, WindowChangedArgs,
WindowChrome, WindowIcon, WindowRoot, WindowVars, FRAME_IMAGE_READY_EVENT, MONITORS, MONITORS_CHANGED_EVENT, WINDOWS,
WINDOW_CHANGED_EVENT,
AutoSize, CursorImage, FrameCaptureMode, FrameImageReadyArgs, HeadlessMonitor, MonitorInfo, StartPosition, WidgetInfoImeArea,
WindowChangedArgs, WindowChrome, WindowIcon, WindowRoot, WindowVars, FRAME_IMAGE_READY_EVENT, MONITORS, MONITORS_CHANGED_EVENT,
WINDOWS, WINDOW_CHANGED_EVENT, WINDOW_FOCUS,
};
struct ImageResources {
@ -506,17 +507,11 @@ impl HeadedCtrl {
if let Some(e) = self.vars.0.access_enabled.get_new() {
debug_assert!(e.is_enabled());
UPDATES.update_info_window(WINDOW.id());
} else {
let access = self.vars.0.access_enabled.get();
if access.contains(AccessEnabled::APP) && LANG_VAR.is_new() {
UPDATES.update_info_window(WINDOW.id());
}
if access == AccessEnabled::VIEW && FOCUS.focused().is_new() {
self.update_access_focused();
}
} else if self.vars.0.access_enabled.get() == AccessEnabled::VIEW && WINDOW_FOCUS.focused().is_new() {
self.update_access_focused();
}
if super::IME_EVENT.has_subscribers() && FOCUS.focused().is_new() {
if super::IME_EVENT.has_subscribers() && WINDOW_FOCUS.focused().is_new() {
self.update_ime();
}
@ -801,7 +796,7 @@ impl HeadedCtrl {
fn accessible_focused(&self, info: &WidgetInfoTree) -> Option<WidgetId> {
if WINDOWS.is_focused(info.window_id()).unwrap_or(false) {
FOCUS.focused().with(|p| {
WINDOW_FOCUS.focused().with(|p| {
if let Some(p) = p {
if p.window_id() == info.window_id() {
if let Some(wgt) = info.get(p.widget_id()) {
@ -839,7 +834,7 @@ impl HeadedCtrl {
}
fn update_ime(&mut self) {
FOCUS.focused().with(|f| {
WINDOW_FOCUS.focused().with(|f| {
let mut ime_path = None;
if let Some(f) = f {
if f.interactivity().is_enabled() && f.window_id() == WINDOW.id() && super::IME_EVENT.is_subscriber(f.widget_id()) {
@ -1414,11 +1409,6 @@ impl HeadlessWithRendererCtrl {
if let Some(e) = self.vars.0.access_enabled.get_new() {
debug_assert!(e.is_enabled());
UPDATES.update_info_window(WINDOW.id());
} else {
let access = self.vars.0.access_enabled.get();
if access.contains(AccessEnabled::APP) && LANG_VAR.is_new() {
UPDATES.update_info_window(WINDOW.id());
}
}
if self.surface.is_some() {
@ -1677,11 +1667,6 @@ impl HeadlessCtrl {
if let Some(e) = self.vars.0.access_enabled.get_new() {
debug_assert!(e.is_enabled());
UPDATES.update_info_window(WINDOW.id());
} else {
let access = self.vars.0.access_enabled.get();
if access.contains(AccessEnabled::APP) && LANG_VAR.is_new() {
UPDATES.update_info_window(WINDOW.id());
}
}
if self.vars.size().is_new() || self.vars.min_size().is_new() || self.vars.max_size().is_new() || self.vars.auto_size().is_new() {
@ -1797,7 +1782,7 @@ impl HeadlessSimulator {
}
fn enabled(&mut self) -> bool {
*self.is_enabled.get_or_insert_with(|| zero_ui_app::App::window_mode().is_headless())
*self.is_enabled.get_or_insert_with(|| zero_ui_app::APP.window_mode().is_headless())
}
pub fn pre_event(&mut self, update: &EventUpdate) {
@ -1837,6 +1822,8 @@ enum InitState {
Inited,
}
type RenderColor = zero_ui_view_api::webrender_api::ColorF;
/// Implementer of window UI node tree initialization and management.
struct ContentCtrl {
vars: WindowVars,
@ -1849,8 +1836,6 @@ struct ContentCtrl {
init_state: InitState,
frame_id: FrameId,
clear_color: RenderColor,
previous_transforms: IdMap<WidgetId, PxTransform>,
}
impl ContentCtrl {
pub fn new(vars: WindowVars, commands: WindowCommands, window: WindowRoot) -> Self {
@ -1866,8 +1851,6 @@ impl ContentCtrl {
init_state: InitState::SkipOne,
frame_id: FrameId::INVALID,
clear_color: RenderColor::BLACK,
previous_transforms: IdMap::default(),
}
}
@ -1917,10 +1900,6 @@ impl ContentCtrl {
self.vars.0.scale_factor.get(),
);
if let Some(mut access) = info.access() {
access.set_lang(LANG_VAR.with(|l| l.best().clone()));
}
WIDGET.with_context(&mut self.root_ctx, WidgetUpdateMode::Bubble, || {
self.root.info(&mut info);
});
@ -2056,8 +2035,6 @@ impl ContentCtrl {
self.frame_id = self.frame_id.next();
let default_text_aa = FONTS.system_font_aa().get();
let mut frame = FrameBuilder::new(
render_widgets,
render_update_widgets,
@ -2067,7 +2044,7 @@ impl ContentCtrl {
&WINDOW.info(),
renderer.clone(),
scale_factor,
default_text_aa,
FontAntiAliasing::Default,
);
let frame = WIDGET.with_context(&mut self.root_ctx, WidgetUpdateMode::Bubble, || {
@ -2075,8 +2052,6 @@ impl ContentCtrl {
frame.finalize(&WINDOW.info())
});
self.notify_transform_changes();
self.clear_color = frame.clear_color;
let capture = self.take_frame_capture();
@ -2114,8 +2089,6 @@ impl ContentCtrl {
update.finalize(&WINDOW.info())
});
self.notify_transform_changes();
if let Some(c) = update.clear_color {
self.clear_color = c;
}
@ -2154,34 +2127,6 @@ impl ContentCtrl {
FrameCaptureMode::AllMask(m) => FrameCapture::Mask(m),
}
}
fn notify_transform_changes(&mut self) {
let mut changes_count = 0;
TRANSFORM_CHANGED_EVENT.visit_subscribers(|wid| {
let tree = WINDOW.info();
if let Some(wgt) = tree.get(wid) {
let transform = wgt.bounds_info().inner_transform();
match self.previous_transforms.entry(wid) {
IdEntry::Occupied(mut e) => {
let prev = e.insert(transform);
if prev != transform {
TRANSFORM_CHANGED_EVENT.notify(TransformChangedArgs::now(wgt.path(), prev, transform));
changes_count += 1;
}
}
IdEntry::Vacant(e) => {
e.insert(transform);
}
}
}
});
if (self.previous_transforms.len() - changes_count) > 500 {
self.previous_transforms.retain(|k, _| TRANSFORM_CHANGED_EVENT.is_subscriber(*k));
}
}
}
/// Management of window content and synchronization of WindowVars and View-Process.

View File

@ -32,7 +32,7 @@ event_args! {
/// Target.
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
list.insert_path(&self.target);
list.insert_wgt(&self.target);
}
}
}

View File

@ -1,5 +1,8 @@
//! App window and monitors manager.
// suppress nag about very simple boxed closure signatures.
#![allow(clippy::type_complexity)]
#[macro_use]
extern crate bitflags;
@ -27,7 +30,7 @@ use zero_ui_app::{
window::WindowId,
AppExtended, AppExtension, ControlFlow, HeadlessApp,
};
use zero_ui_ext_image::{ImageVar, IMAGES};
use zero_ui_ext_image::{ImageVar, IMAGES_WINDOW};
use zero_ui_view_api::image::ImageMaskMode;
pub mod commands;
@ -53,11 +56,13 @@ pub mod commands;
/// * [`MONITORS`]
///
/// The [`WINDOWS`] service is also setup as the implementer for [`IMAGES`] rendering.
///
/// [`IMAGES`]: zero_ui_ext_image::IMAGES
#[derive(Default)]
pub struct WindowManager {}
impl AppExtension for WindowManager {
fn init(&mut self) {
IMAGES.init_render_windows_service(WINDOWS);
IMAGES_WINDOW.hook_render_windows_service(Box::new(WINDOWS));
}
fn event_preview(&mut self, update: &mut EventUpdate) {
@ -107,25 +112,20 @@ pub trait AppRunWindowExt {
/// # Examples
///
/// ```no_run
/// # use zero_ui_core::app::App;
/// # use zero_ui_core::context::WINDOW;
/// # use zero_ui_core::window::AppRunWindowExt;
/// # macro_rules! Window { ($($tt:tt)*) => { unimplemented!() } }
/// # macro_rules! _demo { () => {
/// App::default().run_window(async {
/// println!("starting app with window {:?}", WINDOW.id());
/// Window! {
/// title = "Window 1";
/// child = Text!("Window 1");
/// }
/// })
/// })
/// # }};
/// ```
///
/// Which is a shortcut for:
/// ```no_run
/// # use zero_ui_core::app::App;
/// # use zero_ui_core::context::WINDOW;
/// # use zero_ui_core::window::WINDOWS;
/// # macro_rules! Window { ($($tt:tt)*) => { unimplemented!() } }
/// ```
/// # macro_rules! _demo { () => {
/// App::default().run(async {
/// WINDOWS.open(async {
/// println!("starting app with window {:?}", WINDOW.id());
@ -135,9 +135,10 @@ pub trait AppRunWindowExt {
/// }
/// });
/// })
/// # }};
/// ```
///
/// [`WINDOW`]: crate::context::WINDOW
/// [`WINDOW`]: zero_ui_app::window::WINDOW
fn run_window<F>(self, new_window: F)
where
F: Future<Output = WindowRoot> + Send + 'static;
@ -156,7 +157,6 @@ impl<E: AppExtension> AppRunWindowExt for AppExtended<E> {
/// Extension trait, adds window control methods to [`HeadlessApp`].
///
/// [`open_window`]: HeadlessAppWindowExt::open_window
/// [`HeadlessApp`]: app::HeadlessApp
pub trait HeadlessAppWindowExt {
/// Open a new headless window and returns the new window ID.
///
@ -164,7 +164,7 @@ pub trait HeadlessAppWindowExt {
///
/// Returns the [`WindowId`] of the new window after the window is open and loaded and has generated one frame.
///
/// [`WINDOW`]: crate::context::WINDOW
/// [`WINDOW`]: zero_ui_app::window::WINDOW
fn open_window<F>(&mut self, new_window: F) -> WindowId
where
F: Future<Output = WindowRoot> + Send + 'static;
@ -192,7 +192,7 @@ impl HeadlessAppWindowExt for HeadlessApp {
where
F: Future<Output = WindowRoot> + Send + 'static,
{
zero_ui_app::App::extensions().require::<WindowManager>();
zero_ui_app::APP.extensions().require::<WindowManager>();
let response = WINDOWS.open(new_window);
self.run_task(async move {

View File

@ -1,19 +1,19 @@
use core::fmt;
use std::sync::Arc;
use zero_ui_app::event::{event, event_args};
use zero_ui_app::event::{event, event_args, AnyEventArgs};
use zero_ui_app::update::EventUpdate;
use zero_ui_app::view_process::raw_events::{RawMonitorsChangedArgs, RAW_MONITORS_CHANGED_EVENT, RAW_SCALE_FACTOR_CHANGED_EVENT};
use zero_ui_app::view_process::VIEW_PROCESS_INITED_EVENT;
use zero_ui_app::view_process::raw_events::{RawMonitorsChangedArgs, RAW_SCALE_FACTOR_CHANGED_EVENT, RAW_MONITORS_CHANGED_EVENT};
use zero_ui_app::window::{MonitorId, WINDOW};
use zero_ui_app_context::app_local;
use zero_ui_layout::units::{DipSize, Factor, Ppi, PxPoint, PxSize, Dip, PxRect, DipRect};
use zero_ui_txt::Txt;
use zero_ui_layout::units::{Dip, DipRect, DipSize, DipToPx, Factor, FactorUnits, Ppi, PxPoint, PxRect, PxSize, PxToDip};
use zero_ui_txt::{ToText, Txt};
use zero_ui_unique_id::IdMap;
use zero_ui_var::{impl_from_and_into_var, ArcVar, ReadOnlyArcVar, VarValue, var, Var};
use zero_ui_var::{impl_from_and_into_var, var, ArcVar, ReadOnlyArcVar, Var, VarValue};
use zero_ui_view_api::window::VideoMode;
use crate::WINDOWS;
use crate::{WINDOW_Ext, WINDOWS};
app_local! {
pub(super) static MONITORS_SV: MonitorsService = const {
@ -59,9 +59,9 @@ app_local! {
///
/// [`ppi`]: MonitorInfo::ppi
/// [`scale_factor`]: MonitorInfo::scale_factor
/// [`LayoutMetrics`]: crate::context::LayoutMetrics
/// [`LayoutMetrics`]: zero_ui_layout::context::LayoutMetrics
/// [The Virtual Screen]: https://docs.microsoft.com/en-us/windows/win32/gdi/the-virtual-screen
/// [`WindowManager`]: crate::window::WindowManager
/// [`WindowManager`]: crate::WindowManager
pub struct MONITORS;
impl MONITORS {
/// Get monitor info.
@ -136,7 +136,7 @@ impl MonitorsService {
/// "Monitor" configuration used by windows in [headless mode].
///
/// [headless mode]: crate::window::WindowMode::is_headless
/// [headless mode]: zero_ui_app::window::WindowMode::is_headless
#[derive(Clone, Copy, PartialEq)]
pub struct HeadlessMonitor {
/// The scale factor used for the headless layout and rendering.
@ -145,7 +145,7 @@ pub struct HeadlessMonitor {
///
/// `None` by default.
///
/// [`parent`]: crate::window::WindowVars::parent
/// [`parent`]: crate::WindowVars::parent
pub scale_factor: Option<Factor>,
/// Size of the imaginary monitor screen that contains the headless window.

View File

@ -1,8 +1,9 @@
use std::{fmt, future::Future, mem, sync::Arc};
use std::{any::Any, fmt, future::Future, mem, sync::Arc};
use parking_lot::Mutex;
use zero_ui_app::{
app_hn_once,
event::AnyEventArgs,
timer::{DeadlineHandle, TIMERS},
update::{EventUpdate, InfoUpdates, LayoutUpdates, RenderUpdates, UpdateOp, WidgetUpdates, UPDATES},
view_process::{
@ -14,21 +15,27 @@ use zero_ui_app::{
ViewImage, ViewRenderer, VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT,
},
widget::{
info::{WidgetInfo, WidgetInfoChangedArgs, WidgetInfoTree},
instance::{BoxedUiNode, NilUiNode},
WidgetId,
info::{InteractionPath, WidgetInfo, WidgetInfoTree},
instance::{BoxedUiNode, NilUiNode, UiNode},
UiTaskWidget, WidgetId,
},
window::{WindowCtx, WindowId, WINDOW},
AppEventSender, APP_PROCESS, EXIT_REQUESTED_EVENT,
window::{WindowCtx, WindowId, WindowMode, WINDOW},
AppEventSender, APP, EXIT_REQUESTED_EVENT,
};
use zero_ui_app_context::app_local;
use zero_ui_color::COLOR_SCHEME_VAR;
use zero_ui_ext_image::{ImageRenderWindowRoot, ImageRenderWindowsService, ImageVar, Img};
use zero_ui_layout::units::{Deadline, Factor, PxRect};
use zero_ui_task::ui::UiTask;
use zero_ui_layout::units::{Deadline, Factor, FactorUnits, LengthUnits, PxRect, TimeUnits};
use zero_ui_task::{
rayon::iter::{IntoParallelRefMutIterator, ParallelIterator},
ui::UiTask,
ParallelIteratorExt,
};
use zero_ui_txt::{formatx, Txt};
use zero_ui_unique_id::{IdMap, IdSet};
use zero_ui_var::{impl_from_and_into_var, response_done_var, response_var, var, ArcVar, ResponderVar, ResponseVar};
use zero_ui_var::{
impl_from_and_into_var, response_done_var, response_var, var, ArcVar, BoxedVar, LocalVar, ResponderVar, ResponseVar, Var,
};
use zero_ui_view_api::{
config::ColorScheme,
image::ImageMaskMode,
@ -38,13 +45,14 @@ use zero_ui_view_api::{
use crate::{
commands::WindowCommands, control::WindowCtrl, CloseWindowResult, FrameCaptureMode, HeadlessMonitor, StartPosition, WindowChrome,
WindowCloseArgs, WindowCloseRequestedArgs, WindowFocusChangedArgs, WindowMode, WindowNotFound, WindowOpenArgs, WindowRoot, WindowVars,
WindowCloseArgs, WindowCloseRequestedArgs, WindowFocusChangedArgs, WindowNotFound, WindowOpenArgs, WindowRoot, WindowVars,
FRAME_IMAGE_READY_EVENT, MONITORS, WINDOW_CLOSE_EVENT, WINDOW_CLOSE_REQUESTED_EVENT, WINDOW_FOCUS_CHANGED_EVENT, WINDOW_LOAD_EVENT,
WINDOW_VARS_ID,
};
app_local! {
pub(super) static WINDOWS_SV: WindowsService = WindowsService::new();
static FOCUS_SV: BoxedVar<Option<InteractionPath>> = LocalVar(None).boxed();
}
pub(super) struct WindowsService {
exit_on_last_close: ArcVar<bool>,
@ -77,9 +85,7 @@ impl WindowsService {
Self {
exit_on_last_close: var(true),
default_render_mode: var(RenderMode::default()),
root_extenders: Mutex::new(vec![Box::new(|a| {
with_context_var_init(a.root, COLOR_SCHEME_VAR, || WINDOW.vars().actual_color_scheme().boxed()).boxed()
})]),
root_extenders: Mutex::new(vec![]),
parallel: var(ParallelWin::default()),
windows: IdMap::default(),
windows_info: IdMap::default(),
@ -242,6 +248,8 @@ impl_from_and_into_var! {
/// # Provider
///
/// This service is provided by the [`WindowManager`].
///
/// [`WindowManager`]: crate::WindowManager
pub struct WINDOWS;
impl WINDOWS {
/// If app process exit is requested when a window closes and there are no more windows open, `true` by default.
@ -267,10 +275,8 @@ impl WINDOWS {
///
/// All parallel is enabled by default. See [`ParallelWin`] for details of what parts of the windows can update in parallel.
///
/// Note that this config is for parallel execution between windows, see the [`parallel`] property for parallel execution
/// Note that this config is for parallel execution between windows, see the `parallel` property for parallel execution
/// within windows and widgets.
///
/// [`parallel`]: fn@crate::widget_base::parallel
pub fn parallel(&self) -> ArcVar<ParallelWin> {
WINDOWS_SV.read().parallel.clone()
}
@ -553,16 +559,13 @@ impl WINDOWS {
/// Requests that the window be made the foreground keyboard focused window.
///
/// Prefer using the [`FOCUS`] service and advanced [`FocusRequest`] configs instead of using this method directly.
/// Prefer using the `FOCUS` service and advanced `FocusRequest` configs instead of using this method directly.
///
/// This operation can steal keyboard focus from other apps disrupting the user, be careful with it.
///
/// If the `window_id` is only associated with an open request it is modified to focus the window on open.
///
/// If more than one focus request is made in the same update cycle only the last request is processed.
///
/// [`FOCUS`]: crate::focus::FOCUS
/// [`FocusRequest`]: crate::focus::FocusRequest
pub fn focus(&self, window_id: impl Into<WindowId>) -> Result<(), WindowNotFound> {
let window_id = window_id.into();
if !self.is_focused(window_id)? {
@ -647,8 +650,7 @@ impl WINDOWS {
/// Update widget info tree associated with the window.
pub(super) fn set_widget_tree(&self, info_tree: WidgetInfoTree) {
if let Some(info) = WINDOWS_SV.write().windows_info.get_mut(&info_tree.window_id()) {
let prev_tree = info.widget_tree.clone();
info.widget_tree = info_tree.clone();
info.widget_tree = info_tree;
}
}
@ -798,7 +800,7 @@ impl WINDOWS {
}
}
let is_headless_app = zero_ui_app::App::window_mode().is_headless();
let is_headless_app = zero_ui_app::APP.window_mode().is_headless();
let wns = WINDOWS_SV.read();
// if set to exit on last headed window close in a headed app,
@ -813,7 +815,7 @@ impl WINDOWS {
&& !wns.open_tasks.iter().any(|t| matches!(t.mode, WindowMode::Headed))
{
// fulfill `exit_on_last_close`
APP_PROCESS.exit();
APP.exit();
}
} else if let Some(args) = EXIT_REQUESTED_EVENT.on(update) {
if !args.propagation().is_stopped() {
@ -863,7 +865,7 @@ impl WINDOWS {
(wns.take_requests(), wns.latest_color_scheme)
};
let window_mode = zero_ui_app::App::window_mode();
let window_mode = zero_ui_app::APP.window_mode();
// fulfill open requests.
for r in open {
@ -1514,7 +1516,11 @@ pub struct WindowRootExtenderArgs {
pub root: BoxedUiNode,
}
impl ImageRenderWindowRoot for WindowRoot {}
impl ImageRenderWindowRoot for WindowRoot {
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
impl ImageRenderWindowsService for WINDOWS {
fn new_window_root(&self, node: BoxedUiNode, render_mode: RenderMode, scale_factor: Option<Factor>) -> Box<dyn ImageRenderWindowRoot> {
@ -1544,20 +1550,23 @@ impl ImageRenderWindowsService for WINDOWS {
vars.parent().set(parent_id);
}
fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot>>) {
fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>) {
WINDOWS.open_headless(
async move {
let w = new_window_root();
let w = *new_window_root()
.into_any()
.downcast::<WindowRoot>()
.expect("expected `WindowRoot` in image render window");
let vars = WINDOW.vars();
vars.auto_size().set(true);
vars.min_size().set((1.px(), 1.px()));
w
},
true,
)
);
}
fn on_frame_image_ready(&self, update: &EventUpdate) -> (WindowId, Img) {
fn on_frame_image_ready(&self, update: &EventUpdate) -> Option<(WindowId, Img)> {
if let Some(args) = FRAME_IMAGE_READY_EVENT.on(update) {
if let Some(img) = &args.frame_image {
return Some((args.window_id, img.clone()));
@ -1574,3 +1583,19 @@ impl ImageRenderWindowsService for WINDOWS {
Box::new(WINDOWS)
}
}
/// Window focused widget hook.
#[allow(non_camel_case_types)]
pub struct WINDOW_FOCUS;
impl WINDOW_FOCUS {
/// Setup a var that is controlled by the focus service and tracks the focused widget.
///
/// This must be called by the focus implementation only.
pub fn hook_focus_service(&self, focused: BoxedVar<Option<InteractionPath>>) {
*FOCUS_SV.write() = focused;
}
pub(crate) fn focused(&self) -> BoxedVar<Option<InteractionPath>> {
FOCUS_SV.get()
}
}

View File

@ -12,23 +12,25 @@ use zero_ui_app::{
},
window::{WindowId, WINDOW},
};
use zero_ui_ext_image::{ImageSource, ImageVar, Img};
use zero_ui_layout::units::{DipPoint, DipSize, PxPoint};
use zero_ui_txt::Txt;
use zero_ui_unique_id::IdSet;
use zero_ui_var::impl_from_and_into_var;
use zero_ui_view_api::{
api_extension::ApiExtensionId,
api_extension::{ApiExtensionId, ApiExtensionPayload},
image::{ImageDataFormat, ImageMaskMode},
webrender_api::DebugFlags,
window::{EventCause, FrameId, RenderMode, WindowState},
};
use crate::HeadlessMonitor;
use crate::{HeadlessMonitor, WINDOW_Ext};
/// Window root widget and configuration.
///
/// More window configuration is accessible using the [`WindowVars`] type.
///
/// [`WindowVars`]: crate::window::WindowVars
/// [`WindowVars`]: crate::WindowVars
pub struct WindowRoot {
pub(super) id: WidgetId,
pub(super) start_position: StartPosition,
@ -48,7 +50,7 @@ impl WindowRoot {
/// from accidentally exiting full-screen. Also causes subsequent open windows to be child of this window.
/// * `transparent` - If the window should be created in a compositor mode that renders semi-transparent pixels as "see-through".
/// * `render_mode` - Render mode preference overwrite for this window, note that the actual render mode selected can be different.
/// * `headless_monitor` - "Monitor" configuration used in [headless mode](WindowMode::is_headless).
/// * `headless_monitor` - "Monitor" configuration used in [headless mode](zero_ui_app::window::WindowMode::is_headless).
/// * `start_focused` - If the window is forced to be the foreground keyboard focus after opening.
/// * `root` - The root widget's outermost `CONTEXT` node, the window uses this and the `root_id` to form the root widget.
#[allow(clippy::too_many_arguments)]
@ -186,57 +188,6 @@ impl fmt::Debug for StartPosition {
}
}
/// Mode of an open window.
#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum WindowMode {
/// Normal mode, shows a system window with content rendered.
Headed,
/// Headless mode, no system window and no renderer. The window does layout and calls [`UiNode::render`] but
/// it does not actually generates frame textures.
Headless,
/// Headless mode, no visible system window but with a renderer. The window does everything a [`Headed`](WindowMode::Headed)
/// window does, except presenting frame textures in a system window.
HeadlessWithRenderer,
}
impl fmt::Debug for WindowMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "WindowMode::")?;
}
match self {
WindowMode::Headed => write!(f, "Headed"),
WindowMode::Headless => write!(f, "Headless"),
WindowMode::HeadlessWithRenderer => write!(f, "HeadlessWithRenderer"),
}
}
}
impl WindowMode {
/// If is the [`Headed`](WindowMode::Headed) mode.
pub fn is_headed(self) -> bool {
match self {
WindowMode::Headed => true,
WindowMode::Headless | WindowMode::HeadlessWithRenderer => false,
}
}
/// If is the [`Headless`](WindowMode::Headed) or [`HeadlessWithRenderer`](WindowMode::Headed) modes.
pub fn is_headless(self) -> bool {
match self {
WindowMode::Headless | WindowMode::HeadlessWithRenderer => true,
WindowMode::Headed => false,
}
}
/// If is the [`Headed`](WindowMode::Headed) or [`HeadlessWithRenderer`](WindowMode::HeadlessWithRenderer) modes.
pub fn has_renderer(self) -> bool {
match self {
WindowMode::Headed | WindowMode::HeadlessWithRenderer => true,
WindowMode::Headless => false,
}
}
}
/// Window chrome, the non-client area of the window.
#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum WindowChrome {
@ -315,7 +266,7 @@ pub enum WindowIcon {
Default,
/// Image is requested from [`IMAGES`].
///
/// [`IMAGES`]: crate::image::IMAGES
/// [`IMAGES`]: zero_ui_ext_image::IMAGES
Image(ImageSource),
}
impl fmt::Debug for WindowIcon {
@ -345,14 +296,12 @@ impl WindowIcon {
/// to cause the icon to re-render when the node it-self updates. Note that just because you can update the icon
/// does not mean that animating it is a good idea.
///
/// [`image::render_retain`]: fn@crate::image::render_retain
/// [`image::render_retain`]: fn@zero_ui_ext_image::render_retain
///
/// # Examples
///
/// ```
/// # use zero_ui_core::{window::WindowIcon, render::RenderMode};
/// # macro_rules! Container { ($($tt:tt)*) => { zero_ui_core::widget_instance::NilUiNode } }
/// # let _ =
/// # macro_rules! _demo { () => {
/// WindowIcon::render(
/// || Container! {
/// // image::render_retain = true;
@ -364,7 +313,7 @@ impl WindowIcon {
/// child = Text!("A");
/// }
/// )
/// # ;
/// # }};
/// ```
pub fn render<I, F>(new_icon: F) -> Self
where
@ -468,13 +417,13 @@ pub struct CursorImage {
///
/// You can set the capture mode using [`WindowVars::frame_capture_mode`].
///
/// [`WindowVars::frame_capture_mode`]: crate::window::WindowVars::frame_capture_mode
/// [`WindowVars::frame_capture_mode`]: crate::WindowVars::frame_capture_mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum FrameCaptureMode {
/// Frames are not automatically captured, but you can
/// use [`WINDOWS.frame_image`] to capture frames.
///
/// [`WINDOWS.frame_image`]: crate::window::WINDOWS.frame_image
/// [`WINDOWS.frame_image`]: crate::WINDOWS.frame_image
Sporadic,
/// The next rendered frame will be captured and available in [`FrameImageReadyArgs::frame_image`]
/// as a full BGRA8 image.
@ -748,15 +697,15 @@ event! {
///
/// You can request a copy of the pixels using [`WINDOWS.frame_image`] or by setting the [`WindowVars::frame_capture_mode`].
///
/// [`WINDOWS.frame_image`]: crate::window::WINDOWS::frame_image
/// [`WindowVars::frame_capture_mode`]: crate::window::WindowVars::frame_capture_mode
/// [`WINDOWS.frame_image`]: crate::WINDOWS::frame_image
/// [`WindowVars::frame_capture_mode`]: crate::WindowVars::frame_capture_mode
pub static FRAME_IMAGE_READY_EVENT: FrameImageReadyArgs;
}
/// Response message of [`close`] and [`close_together`].
///
/// [`close`]: crate::window::WINDOWS::close
/// [`close_together`]: crate::window::WINDOWS::close_together
/// [`close`]: crate::WINDOWS::close
/// [`close_together`]: crate::WINDOWS::close_together
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CloseWindowResult {
/// Operation completed, all requested windows closed.
@ -768,7 +717,7 @@ pub enum CloseWindowResult {
/// Error when a [`WindowId`] is not opened by the [`WINDOWS`] service.
///
/// [`WINDOWS`]: crate::window::WINDOWS
/// [`WINDOWS`]: crate::WINDOWS
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct WindowNotFound(pub WindowId);
impl fmt::Display for WindowNotFound {
@ -778,10 +727,6 @@ impl fmt::Display for WindowNotFound {
}
impl std::error::Error for WindowNotFound {}
impl_from_and_into_var! {
fn from(some: WindowId) -> Option<WindowId>;
}
/// Webrender renderer debug flags and profiler UI.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct RendererDebug {
@ -855,10 +800,10 @@ impl RendererDebug {
.flatten()
}
pub(super) fn push_extension(&self, exts: &mut Vec<(ApiExtensionId, zero_ui_view_api::api_extension::ApiExtensionPayload)>) {
pub(super) fn push_extension(&self, exts: &mut Vec<(ApiExtensionId, ApiExtensionPayload)>) {
if !self.is_empty() {
if let Some(id) = self.extension_id() {
exts.push((id, zero_ui_view_api::ApiExtensionPayload::serialize(self).unwrap()));
exts.push((id, ApiExtensionPayload::serialize(self).unwrap()));
}
}
}

View File

@ -5,11 +5,13 @@ use zero_ui_app::{
window::{MonitorId, WindowId, WINDOW},
};
use zero_ui_ext_image::Img;
use zero_ui_layout::units::{DipPoint, DipRect, DipSize, Factor, Length, Point, PxPoint, Size, PxSize, Dip};
use zero_ui_layout::units::{
Dip, DipPoint, DipRect, DipSize, DipToPx, Factor, FactorUnits, Length, LengthUnits, Point, PxPoint, PxSize, Size,
};
use zero_ui_state_map::StaticStateId;
use zero_ui_txt::Txt;
use zero_ui_txt::{ToText, Txt};
use zero_ui_unique_id::IdSet;
use zero_ui_var::{ArcVar, var, ReadOnlyArcVar, BoxedVar, merge_var};
use zero_ui_var::{merge_var, var, ArcVar, BoxedVar, ReadOnlyArcVar, Var};
use zero_ui_view_api::{
config::ColorScheme,
window::{CursorIcon, FocusIndicator, RenderMode, VideoMode, WindowState},
@ -83,8 +85,8 @@ pub(super) struct WindowVarsData {
///
/// You can get the controller for the current context window using [`WINDOW.vars`].
///
/// [`WINDOWS.vars`]: crate::window::WINDOWS::vars
/// [`WINDOW.vars`]: crate::window::WINDOW_Ext::vars
/// [`WINDOWS.vars`]: crate::WINDOWS::vars
/// [`WINDOW.vars`]: crate::WINDOW_Ext::vars
#[derive(Clone)]
pub struct WindowVars(pub(super) Arc<WindowVarsData>);
impl WindowVars {
@ -449,6 +451,7 @@ impl WindowVars {
/// The default value is [`Point::top_left`].
///
/// [`auto_size`]: Self::auto_size
/// [`StartPosition`]: crate::StartPosition
pub fn auto_size_origin(&self) -> ArcVar<Point> {
self.0.auto_size_origin.clone()
}
@ -613,7 +616,7 @@ impl WindowVars {
///
/// A window is only opened in the view-process once it is loaded, see [`WINDOWS.loading_handle`] for more details.
///
/// [`WINDOWS.loading_handle`]: crate::window::WINDOWS::loading_handle
/// [`WINDOWS.loading_handle`]: crate::WINDOWS::loading_handle
pub fn is_loaded(&self) -> ReadOnlyArcVar<bool> {
self.0.is_loaded.read_only()
}
@ -623,10 +626,7 @@ impl WindowVars {
/// This is usually a visual indication on the taskbar icon that prompts the user to focus on the window, it is automatically
/// changed to `None` once the window receives focus or you can set it to `None` to cancel the indicator.
///
/// Prefer using the [`FOCUS`] service and advanced [`FocusRequest`] configs instead of setting this variable directly.
///
/// [`FOCUS`]: crate::focus::FOCUS
/// [`FocusRequest`]: crate::focus::FocusRequest
/// Prefer using the `FOCUS` service and advanced `FocusRequest` configs instead of setting this variable directly.
pub fn focus_indicator(&self) -> ArcVar<Option<FocusIndicator>> {
self.0.focus_indicator.clone()
}
@ -639,7 +639,7 @@ impl WindowVars {
///
/// [`Next`]: FrameCaptureMode::Next
/// [`Sporadic`]: FrameCaptureMode::Sporadic
/// [`WIDGET.render_update`]: crate::context::WIDGET::render_update
/// [`WIDGET.render_update`]: zero_ui_app::widget::WIDGET::render_update
pub fn frame_capture_mode(&self) -> ArcVar<FrameCaptureMode> {
self.0.frame_capture_mode.clone()
}
@ -649,7 +649,7 @@ impl WindowVars {
/// The initial value is the [`default_render_mode`], it can update after the window is created, when the view-process
/// actually creates the backend window and after a view-process respawn.
///
/// [`default_render_mode`]: crate::window::WINDOWS::default_render_mode
/// [`default_render_mode`]: crate::WINDOWS::default_render_mode
pub fn render_mode(&self) -> ReadOnlyArcVar<RenderMode> {
self.0.render_mode.read_only()
}

View File

@ -21,6 +21,7 @@ dyn_closure = ["zero-ui-var/dyn_closure"]
[dependencies]
zero-ui-clone_move = { path = "../zero-ui-clone_move" }
zero-ui-app = { path = "../zero-ui-app" }
zero-ui-app_context = { path = "../zero-ui-app_context" }
zero-ui-var = { path = "../zero-ui-var" }
zero-ui-state_map = { path = "../zero-ui-state_map" }
zero-ui-layout = { path = "../zero-ui-layout" }
@ -30,4 +31,4 @@ tracing = "0.1"
serde = { version = "1", features = ["derive"] }
[package.metadata.docs.rs]
rustdoc-args = ["--html-in-header", "zero-ui-wgt/doc/html-in-header.html"]
rustdoc-args = ["--html-in-header", "zero-ui-widget/doc/html-in-header.html"]

View File

@ -1,5 +1,8 @@
//! Basic widget properties and helpers for declaring widgets and properties.
// suppress nag about very simple boxed closure signatures.
#![allow(clippy::type_complexity)]
use std::fmt;
use zero_ui_app::{

View File

@ -2,7 +2,7 @@
//!
//! This module defines some foundational nodes that can be used for declaring properties and widgets.
use std::sync::Arc;
use std::{any::Any, sync::Arc};
use zero_ui_app::{
event::{Command, CommandArgs, Event, EventArgs},
@ -14,11 +14,12 @@ use zero_ui_app::{
VarLayout, WIDGET,
},
};
use zero_ui_app_context::{ContextLocal, LocalContext};
use zero_ui_layout::{
context::LAYOUT,
units::{PxConstraints2d, PxCornerRadius, PxPoint, PxRect, PxSideOffsets, PxSize, PxVector, SideOffsets},
};
use zero_ui_state_map::StateMapRef;
use zero_ui_state_map::{StateId, StateMapRef, StateValue};
use zero_ui_var::*;
#[doc(hidden)]
@ -43,7 +44,7 @@ pub use zero_ui_app;
/// # fn main() -> () { }
/// # use zero_ui_app::{*, widget::{instance::*, *}};
/// # use zero_ui_var::*;
/// # use zero_ui_wgt::nodes::*;
/// # use zero_ui_widget::nodes::*;
/// #
/// context_var! {
/// pub static FOO_VAR: u32 = 0u32;
@ -69,7 +70,7 @@ pub use zero_ui_app;
/// # fn main() -> () { }
/// # use zero_ui_app::{*, widget::{instance::*, *}};
/// # use zero_ui_var::*;
/// # use zero_ui_wgt::nodes::*;
/// # use zero_ui_widget::nodes::*;
/// #
/// #[derive(Debug, Clone, Default, PartialEq)]
/// pub struct Config {
@ -293,7 +294,7 @@ macro_rules! __event_property {
/// ```
/// # fn main() { }
/// # use zero_ui_app::event::*;
/// # use zero_ui_wgt::nodes::*;
/// # use zero_ui_widget::nodes::*;
/// # #[derive(Clone, Debug, PartialEq)] pub enum KeyState { Pressed }
/// # event_args! { pub struct KeyInputArgs { pub state: KeyState, .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) { } } }
/// # event! { pub static KEY_INPUT_EVENT: KeyInputArgs; }
@ -343,7 +344,7 @@ macro_rules! __event_property {
/// ```
/// # fn main() { }
/// # use zero_ui_app::{event::*, widget::instance::UiNode};
/// # use zero_ui_wgt::nodes::*;
/// # use zero_ui_widget::nodes::*;
/// # event_args! { pub struct KeyInputArgs { .. fn delivery_list(&self, _l: &mut UpdateDeliveryList) {} } }
/// # event! { pub static KEY_INPUT_EVENT: KeyInputArgs; }
/// # fn some_node(child: impl UiNode) -> impl UiNode { child }
@ -596,7 +597,7 @@ macro_rules! __command_property {
/// # fn main() { }
/// # use zero_ui_app::{event::*, widget::*};
/// # use zero_ui_app::var::*;
/// # use zero_ui_wgt::nodes::*;
/// # use zero_ui_widget::nodes::*;
/// # command! {
/// # pub static PASTE_CMD;
/// # }
@ -1396,6 +1397,244 @@ pub fn border_node(child: impl UiNode, border_offsets: impl IntoVar<SideOffsets>
})
}
/// Helper for declaring nodes that sets a context local.
pub fn with_context_local<T: Any + Send + Sync + 'static>(
child: impl UiNode,
context: &'static ContextLocal<T>,
value: impl Into<T>,
) -> impl UiNode {
let mut value = Some(Arc::new(value.into()));
match_node(child, move |child, op| {
context.with_context(&mut value, || child.op(op));
})
}
/// Helper for declaring nodes that sets a context local with a value generated on init.
///
/// The method calls the `init_value` closure on init to produce a *value* var that is presented as the [`ContextLocal<T>`]
/// in the widget and widget descendants. The closure can be called more than once if the returned node is reinited.
///
/// Apart from the value initialization this behaves just like [`with_context_local`].
pub fn with_context_local_init<T: Any + Send + Sync + 'static>(
child: impl UiNode,
context: &'static ContextLocal<T>,
init_value: impl FnMut() -> T + Send + 'static,
) -> impl UiNode {
#[cfg(dyn_closure)]
let init_value: Box<dyn FnMut() -> T + Send> = Box::new(init_value);
with_context_local_init_impl(child.cfg_boxed(), context, init_value).cfg_boxed()
}
fn with_context_local_init_impl<T: Any + Send + Sync + 'static>(
child: impl UiNode,
context: &'static ContextLocal<T>,
mut init_value: impl FnMut() -> T + Send + 'static,
) -> impl UiNode {
let mut value = None;
match_node(child, move |child, op| {
let mut is_deinit = false;
match &op {
UiNodeOp::Init => {
value = Some(Arc::new(init_value()));
}
UiNodeOp::Deinit => {
is_deinit = true;
}
_ => {}
}
context.with_context(&mut value, || child.op(op));
if is_deinit {
value = None;
}
})
}
/// Helper for declaring widgets that are recontextualized to take in some of the context
/// of an *original* parent.
///
/// See [`LocalContext::with_context_blend`] for more details about `over`. The returned
/// node will delegate all node operations to inside the blend. The [`UiNode::with_context`]
/// will delegate to the `child` widget context, but the `ctx` is not blended for this method, only
/// for [`UiNodeOp`] methods.
///
/// # Warning
///
/// Properties, context vars and context locals are implemented with the assumption that all consumers have
/// released the context on return, that is even if the context was shared with worker threads all work was block-waited.
/// This node breaks this assumption, specially with `over: true` you may cause unexpected behavior if you don't consider
/// carefully what context is being captured and what context is being replaced.
///
/// As a general rule, only capture during init or update in [`NestGroup::CHILD`], only wrap full widgets and only place the wrapped
/// widget in a parent's [`NestGroup::CHILD`] for a parent that has no special expectations about the child.
///
/// As an example of things that can go wrong, if you capture during layout, the `LAYOUT` context is captured
/// and replaces `over` the actual layout context during all subsequent layouts in the actual parent.
///
/// # Panics
///
/// Panics during init if `ctx` is not from the same app as the init context.
///
/// [`NestGroup::CHILD`]: crate::widget_builder::NestGroup::CHILD
pub fn with_context_blend(mut ctx: LocalContext, over: bool, child: impl UiNode) -> impl UiNode {
match_widget(child, move |c, op| {
if let UiNodeOp::Init = op {
let init_app = LocalContext::current_app();
ctx.with_context_blend(over, || {
let ctx_app = LocalContext::current_app();
assert_eq!(init_app, ctx_app);
c.op(op)
});
} else {
ctx.with_context_blend(over, || c.op(op));
}
})
}
/// Helper for declaring properties that set the widget state.
///
/// The state ID is set in [`WIDGET`] on init and is kept updated. On deinit it is set to the `default` value.
///
/// # Examples
///
/// ```
/// # fn main() -> () { }
/// use zero_ui_core::{property, context::*, var::IntoVar, widget_instance::UiNode};
///
/// pub static FOO_ID: StaticStateId<u32> = StateId::new_static();
///
/// #[property(CONTEXT)]
/// pub fn foo(child: impl UiNode, value: impl IntoVar<u32>) -> impl UiNode {
/// with_widget_state(child, &FOO_ID, || 0, value)
/// }
///
/// // after the property is used and the widget initializes:
///
/// /// Get the value from outside the widget.
/// fn get_foo_outer(widget: &mut impl UiNode) -> u32 {
/// widget.with_context(WidgetUpdateMode::Ignore, || WIDGET.get_state(&FOO_ID)).flatten().unwrap_or_default()
/// }
///
/// /// Get the value from inside the widget.
/// fn get_foo_inner() -> u32 {
/// WIDGET.get_state(&FOO_ID).unwrap_or_default()
/// }
/// ```
pub fn with_widget_state<U, I, T>(child: U, id: impl Into<StateId<T>>, default: I, value: impl IntoVar<T>) -> impl UiNode
where
U: UiNode,
I: Fn() -> T + Send + 'static,
T: StateValue + VarValue,
{
#[cfg(dyn_closure)]
let default: Box<dyn Fn() -> T + Send> = Box::new(default);
with_widget_state_impl(child.cfg_boxed(), id.into(), default, value.into_var()).cfg_boxed()
}
fn with_widget_state_impl<U, I, T>(child: U, id: impl Into<StateId<T>>, default: I, value: impl IntoVar<T>) -> impl UiNode
where
U: UiNode,
I: Fn() -> T + Send + 'static,
T: StateValue + VarValue,
{
let id = id.into();
let value = value.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
child.init();
WIDGET.sub_var(&value);
WIDGET.set_state(id, value.get());
}
UiNodeOp::Deinit => {
child.deinit();
WIDGET.set_state(id, default());
}
UiNodeOp::Update { updates } => {
child.update(updates);
if let Some(v) = value.get_new() {
WIDGET.set_state(id, v);
}
}
_ => {}
})
}
/// Helper for declaring properties that set the widget state with a custom closure.
///
/// The `default` closure is used to init the state value, then the `modify` closure is used to modify the state using the variable value.
///
/// On deinit the `default` value is set on the state again.
///
/// See [`with_widget_state`] for more details.
pub fn with_widget_state_modify<U, S, V, I, M>(
child: U,
id: impl Into<StateId<S>>,
value: impl IntoVar<V>,
default: I,
modify: M,
) -> impl UiNode
where
U: UiNode,
S: StateValue,
V: VarValue,
I: Fn() -> S + Send + 'static,
M: FnMut(&mut S, &V) + Send + 'static,
{
#[cfg(dyn_closure)]
let default: Box<dyn Fn() -> S + Send> = Box::new(default);
#[cfg(dyn_closure)]
let modify: Box<dyn FnMut(&mut S, &V) + Send> = Box::new(modify);
with_widget_state_modify_impl(child.cfg_boxed(), id.into(), value.into_var(), default, modify)
}
fn with_widget_state_modify_impl<U, S, V, I, M>(
child: U,
id: impl Into<StateId<S>>,
value: impl IntoVar<V>,
default: I,
mut modify: M,
) -> impl UiNode
where
U: UiNode,
S: StateValue,
V: VarValue,
I: Fn() -> S + Send + 'static,
M: FnMut(&mut S, &V) + Send + 'static,
{
let id = id.into();
let value = value.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
child.init();
WIDGET.sub_var(&value);
value.with(|v| {
WIDGET.with_state_mut(|mut s| {
modify(s.entry(id).or_insert_with(&default), v);
})
})
}
UiNodeOp::Deinit => {
child.deinit();
WIDGET.set_state(id, default());
}
UiNodeOp::Update { updates } => {
child.update(updates);
value.with_new(|v| {
WIDGET.with_state_mut(|mut s| {
modify(s.req_mut(id), v);
})
});
}
_ => {}
})
}
#[doc(inline)]
pub use crate::command_property;
#[doc(inline)]