Started refactoring events and focus to own crate.
This commit is contained in:
parent
7c0422130d
commit
3544afe838
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
/// }));
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
///
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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 {}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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![
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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"
|
|
@ -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();
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -32,7 +32,7 @@ event_args! {
|
|||
|
||||
/// Target.
|
||||
fn delivery_list(&self, list: &mut UpdateDeliveryList) {
|
||||
list.insert_path(&self.target);
|
||||
list.insert_wgt(&self.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in New Issue