Started refactoring main crate.

This commit is contained in:
Samuel Guerra 2023-12-07 16:27:17 -03:00
parent a61b8d7959
commit db7042f0c5
72 changed files with 18573 additions and 838 deletions

View File

@ -27,12 +27,6 @@ members = [
# app:
"zero-ui-app-proc-macros",
"zero-ui-app",
"zero-ui-proc-macros",
"zero-ui-core",
"zero-ui",
"zero-ui-material-icons",
# app-extension:
"zero-ui-ext-fs_watcher",
"zero-ui-ext-config",
"zero-ui-ext-font",
@ -44,18 +38,33 @@ members = [
"zero-ui-ext-undo",
# widget:
"zero-ui-widget",
"zero-ui-wgt",
"zero-ui-wgt-access",
"zero-ui-wgt-transform",
"zero-ui-wgt-input",
"zero-ui-wgt-data",
"zero-ui-wgt-filters",
"zero-ui-wgt-inspector",
"zero-ui-wgt-size_offset",
"zero-ui-wgt-container",
"zero-ui-wgt-undo",
"zero-ui-wgt-view",
"zero-ui-wgt-fill",
"zero-ui-wgt-style",
# "zero-ui-wgt-text",
# "zero-ui-wgt-button",
# "zero-ui-material-icons",
# tools:
"zero-ui-l10n-scraper",
# examples:
"examples",
"examples/util",
# "examples",
# "examples/util",
# tests:
"tests",
"tests/build",
# "tests",
# "tests/build",
]
exclude = ["dependencies/webrender"]

View File

@ -45,11 +45,7 @@ WindowManager//WindowId(1) update var of type zero_ui_units::factor::Factor (250
# Split Crates
* Move widget events to wgt crate.
- Implement `on_show`, `on_collapse`.
* Split main crate into widget crates.
- What about properties?
* text crate depends on SCROLL, LAYERS, DATA, ContextMenu.
* Add `WINDOWS.register_root_extender` on the default app?
- `FONTS.system_font_aa`.
@ -70,7 +66,10 @@ with_context_var_init(a.root, COLOR_SCHEME_VAR, || WINDOW.vars().actual_color_sc
* 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,.
- Test what happens when info is rebuild.
- Implement visibility event properties.
* Move `child` and `children` from app to container.
# Publish

View File

@ -42,41 +42,48 @@ pub fn is_rust_analyzer() -> bool {
/// crate name in the crate using our proc-macros. Or, returns `$crate` where `$crate`
/// is the zero-ui-core crate if the crate using our proc-macros does not use the main zero-ui crate.
pub fn crate_core() -> TokenStream {
let (ident, core) = if is_rust_analyzer() {
let (ident, module) = if is_rust_analyzer() {
// rust-analyzer gets the wrong crate sometimes if we cache, maybe they use the same server instance
// for the entire workspace?
let (ident, core) = crate_core_parts();
(Cow::Owned(ident), core)
let (ident, module) = crate_core_parts();
(Cow::Owned(ident), module)
} else {
static CRATE: OnceCell<(String, bool)> = OnceCell::new();
static CRATE: OnceCell<(String, &'static str)> = OnceCell::new();
let (ident, core) = CRATE.get_or_init(crate_core_parts);
(Cow::Borrowed(ident.as_str()), *core)
let (ident, module) = CRATE.get_or_init(crate_core_parts);
(Cow::Borrowed(ident.as_str()), *module)
};
let ident = Ident::new(&ident, Span::call_site());
if core {
quote! { #ident::core }
if !module.is_empty() {
let module = Ident::new(module, Span::call_site());
quote! { #ident::#module }
} else {
ident.to_token_stream()
}
}
fn crate_core_parts() -> (String, bool) {
fn crate_core_parts() -> (String, &'static str) {
if let Ok(ident) = crate_name("zero-ui") {
// using the main crate.
match ident {
FoundCrate::Name(name) => (name, true),
FoundCrate::Itself => ("zero_ui".to_owned(), true),
FoundCrate::Name(name) => (name, "core"),
FoundCrate::Itself => ("crate".to_owned(), "core"),
}
} else if let Ok(ident) = crate_name("zero-ui-wgt") {
// using the main crate.
match ident {
FoundCrate::Name(name) => (name, "__proc_macro_util"),
FoundCrate::Itself => ("zero_ui_app".to_owned(), ""),
}
} else if let Ok(ident) = crate_name("zero-ui-app") {
// using the core crate only.
match ident {
FoundCrate::Name(name) => (name, false),
FoundCrate::Itself => ("zero_ui_app".to_owned(), false),
FoundCrate::Name(name) => (name, ""),
FoundCrate::Itself => ("zero_ui_app".to_owned(), ""),
}
} else {
// failed, at least shows "zero_ui" in the compile error.
("zero_ui".to_owned(), true)
("zero_ui".to_owned(), "core")
}
}

View File

@ -78,7 +78,7 @@ zero-ui-app_context = { path = "../zero-ui-app_context" }
zero-ui-var = { path = "../zero-ui-var" }
zero-ui-txt = { path = "../zero-ui-txt" }
zero-ui-task = { path = "../zero-ui-task" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-view-api = { path = "../zero-ui-view-api", features = ["var"] }
zero-ui-state_map = { path = "../zero-ui-state_map" }
zero-ui-layout = { path = "../zero-ui-layout" }
zero-ui-color = { path = "../zero-ui-color" }

View File

@ -28,6 +28,7 @@ mod tests;
#[doc(hidden)]
#[allow(unused_extern_crates)]
extern crate self as zero_ui_app;
use view_process::VIEW_PROCESS;
use widget::UiTaskWidget;
#[doc(hidden)]

View File

@ -20,7 +20,7 @@ use zero_ui_var::{impl_from_and_into_var, Var, VarCapabilities, VarValue};
use zero_ui_view_api::{
api_extension::{ApiExtensionId, ApiExtensionPayload},
config::FontAntiAliasing,
display_list::{DisplayList, DisplayListBuilder, FilterOp, FrameValue, FrameValueUpdate, NinePatchSource, ReuseRange, ReuseStart},
display_list::{DisplayList, DisplayListBuilder, FilterOp, NinePatchSource, ReuseRange, ReuseStart},
units::PxToWr,
webrender_api::{self, FontRenderMode, GlyphInstance, GlyphOptions, PipelineId, SpatialTreeItemKey},
window::FrameId,
@ -37,6 +37,8 @@ use crate::{
},
};
pub use zero_ui_view_api::display_list::{FrameValue, FrameValueUpdate};
/// A text font.
///
/// This trait is an interface for the renderer into the font API used in the application.

View File

@ -292,13 +292,12 @@ pub use WidgetBaseMacro__ as WidgetBase;
/// [`WidgetBase`]: struct@WidgetBase
pub mod nodes {
use zero_ui_layout::units::{Px, PxCornerRadius, PxRect, PxSize};
use zero_ui_var::IntoVar;
use crate::{
render::{FrameBuilder, FrameUpdate, FrameValueKey},
update::{EventUpdate, WidgetUpdates},
widget::{
info::{Interactivity, WidgetInfoBuilder, WidgetLayout, WidgetMeasure},
info::{WidgetInfoBuilder, WidgetLayout, WidgetMeasure},
instance::BoxedUiNode,
WidgetCtx, WidgetUpdateMode,
},
@ -757,66 +756,6 @@ pub mod nodes {
}
.cfg_boxed()
}
/// Create a node that disables interaction for all widget inside `node` using [`BLOCKED`].
///
/// Unlike the `interactive` property this does not apply to the contextual widget, only `child` and descendants.
///
/// The node works for both if the `child` is a widget or if it contains widgets, the performance
/// is slightly better if the `child` is a widget directly.
///
/// [`BLOCKED`]: Interactivity::BLOCKED
pub fn interactive_node(child: impl UiNode, interactive: impl IntoVar<bool>) -> impl UiNode {
let interactive = interactive.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&interactive);
}
UiNodeOp::Info { info } => {
if interactive.get() {
child.info(info);
} else if let Some(id) = child.with_context(WidgetUpdateMode::Ignore, || WIDGET.id()) {
// child is a widget.
info.push_interactivity_filter(move |args| {
if args.info.id() == id {
Interactivity::BLOCKED
} else {
Interactivity::ENABLED
}
});
child.info(info);
} else {
let block_range = info.with_children_range(|info| child.info(info));
if !block_range.is_empty() {
// has child widgets.
let id = WIDGET.id();
info.push_interactivity_filter(move |args| {
if let Some(parent) = args.info.parent() {
if parent.id() == id {
// check child range
for (i, item) in parent.children().enumerate() {
if item == args.info {
return if !block_range.contains(&i) {
Interactivity::ENABLED
} else {
Interactivity::BLOCKED
};
} else if i >= block_range.end {
break;
}
}
}
}
Interactivity::ENABLED
});
}
}
}
_ => {}
})
}
}
/// Defines the widget innermost node.

View File

@ -1,15 +1,11 @@
use std::hash::Hash;
use parking_lot::Mutex;
use zero_ui_layout::{
context::{InlineConstraints, InlineConstraintsLayout, InlineConstraintsMeasure, InlineSegmentPos, TextSegmentKind, LAYOUT},
units::{about_eq, about_eq_hash, Factor, LayoutMask, Px, PxBox, PxPoint, PxRect, PxSize, PxVector},
context::{InlineConstraints, InlineConstraintsLayout, InlineConstraintsMeasure, InlineSegment, InlineSegmentPos, LAYOUT},
units::{Factor, LayoutMask, Px, PxBox, PxPoint, PxRect, PxSize, PxVector},
};
use zero_ui_state_map::{OwnedStateMap, StateId, StateMapMut, StateValue};
use zero_ui_unique_id::{IdMap, IdSet};
pub use zero_ui_view_api::access::AccessRole;
use crate::{
render::TransformStyle,
update::{InfoUpdates, LayoutUpdates, UpdateFlags},
@ -586,31 +582,6 @@ impl InteractivityChangedArgs {
}
}
/// Represents a segment in an inlined widget first or last row.
///
/// This info is used by inlining parent to sort the joiner row in a way that preserves bidirectional text flow.
///
/// See [`WidgetInlineMeasure::first_segs`] for more details.
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct InlineSegment {
/// Width of the segment, in pixels.
pub width: f32,
/// Info for bidirectional reorder.
pub kind: TextSegmentKind,
}
impl PartialEq for InlineSegment {
fn eq(&self, other: &Self) -> bool {
about_eq(self.width, other.width, 0.001) && self.kind == other.kind
}
}
impl Eq for InlineSegment {}
impl Hash for InlineSegment {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
about_eq_hash(self.width, 0.001, state);
self.kind.hash(state);
}
}
/// Info about the input inline connecting rows of the widget.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WidgetInlineMeasure {

View File

@ -0,0 +1,12 @@
[package]
name = "zero-ui-wgt-access"
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 = { path = "../zero-ui-app" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-wgt = { path = "../zero-ui-wgt" }
zero-ui-ext-l10n = { path = "../zero-ui-ext-l10n" }

View File

@ -0,0 +1,120 @@
use zero_ui_app::access::*;
use zero_ui_view_api::access::AccessCmdName;
use zero_ui_wgt::prelude::*;
event_property! {
/// Access requested a click.
///
/// Note that the normal click event is already triggered by this event.
pub fn access_click {
event: ACCESS_CLICK_EVENT,
args: AccessClickArgs,
with: access_click,
}
/// Access requested expand or collapse the widget content.
pub fn access_expander {
event: ACCESS_EXPANDER_EVENT,
args: AccessExpanderArgs,
with: access_expander,
}
/// Access requested increment or decrement the widget value by steps.
pub fn access_increment {
event: ACCESS_INCREMENT_EVENT,
args: AccessIncrementArgs,
with: access_increment,
}
/// Access requested show or hide the widget's tooltip.
///
/// Note that the tooltip property already handles this event.
pub fn access_tooltip {
event: ACCESS_TOOLTIP_EVENT,
args: AccessToolTipArgs,
with: access_tooltip,
}
/// Access requested a scroll command.
///
/// Note that the scroll widget already handles this event.
pub fn access_scroll {
event: ACCESS_SCROLL_EVENT,
args: AccessScrollArgs,
with: access_scroll,
}
/// Access requested a text input/replace.
///
/// Note that the text widget already handles this event.
pub fn access_text {
event: ACCESS_TEXT_EVENT,
args: AccessTextArgs,
with: access_text,
}
/// Access requested a number input.
pub fn access_number {
event: ACCESS_NUMBER_EVENT,
args: AccessNumberArgs,
with: access_number,
}
/// Access requested a text selection.
///
/// Note that the text widget already handles this event.
pub fn access_selection {
event: ACCESS_SELECTION_EVENT,
args: AccessSelectionArgs,
with: access_selection,
}
}
fn access_click(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::Click)
}
fn access_expander(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::SetExpanded)
}
fn access_increment(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::Increment)
}
fn access_tooltip(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::SetToolTipVis)
}
fn access_scroll(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::Scroll)
}
fn access_text(child: impl UiNode, _: bool) -> impl UiNode {
match_node(child, move |_, op| {
if let UiNodeOp::Info { info } = op {
if let Some(mut access) = info.access() {
access.push_command(AccessCmdName::SetString);
access.push_command(AccessCmdName::ReplaceSelectedText);
}
}
})
}
fn access_number(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::SetNumber)
}
fn access_selection(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::SelectText)
}
fn access_capable(child: impl UiNode, cmd: AccessCmdName) -> impl UiNode {
match_node(child, move |_, op| {
if let UiNodeOp::Info { info } = op {
if let Some(mut access) = info.access() {
access.push_command(cmd)
}
}
})
}

View File

@ -0,0 +1,16 @@
//! Properties that define accessibility metadata.
//!
//! The properties in this crate should only be used by widget implementers, they only
//! define metadata for accessibility, this metadata signals the availability of behaviors
//! that are not implemented by these properties, for example an [`AccessRole::Button`] widget
//! must also be focusable and handle click events, an [`AccessRole::TabList`] must contain widgets
//! marked [`AccessRole::Tab`].
//!
//! [`AccessRole::Button`]: zero_ui_app::widget::info::access::AccessRole::Button
//! [`AccessRole::TabList`]: zero_ui_app::widget::info::access::AccessRole::TabList
//! [`AccessRole::Tab`]: zero_ui_app::widget::info::access::AccessRole::Tab
mod events;
mod meta;
pub use events::*;
pub use meta::*;

View File

@ -0,0 +1,445 @@
use zero_ui_app::widget::info::access::WidgetAccessInfoBuilder;
use zero_ui_ext_l10n::Lang;
use zero_ui_wgt::prelude::*;
use std::num::NonZeroU32;
pub use zero_ui_view_api::access::{
AccessCmdName, AccessRole, AutoComplete, CurrentKind, Invalid, LiveIndicator, Orientation, Popup, SortDirection,
};
/// Sets the widget kind for accessibility services.
///
/// Note that the widget role must be implemented, this property only sets the metadata.
#[property(CONTEXT)]
pub fn access_role(child: impl UiNode, role: impl IntoVar<AccessRole>) -> impl UiNode {
with_access_state(child, role, |b, v| b.set_role(*v))
}
/// Append supported access commands.
#[property(CONTEXT)]
pub fn access_commands(child: impl UiNode, commands: impl IntoVar<Vec<AccessCmdName>>) -> impl UiNode {
with_access_state(child, commands, |b, v| {
for cmd in v {
b.push_command(*cmd);
}
})
}
/// Defines if the widget and descendants can be present in the accessibility info tree.
///
/// If set to `false` the widget and descendants is not included in accessibility info send to screen readers,
/// if set to `true` the widget and descendants can be accessible if they set any accessibility metadata, the
/// same as if this property is not set.
///
/// Note that not accessible widgets will still collect accessibility info, the info is just no send
/// to the view-process and screen readers. Also note that hidden or collapsed widgets are not accessible
/// by default.
#[property(WIDGET, default(true))]
pub fn accessible(child: impl UiNode, accessible: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, accessible, |b, v| {
if !*v {
b.flag_inaccessible();
}
})
}
/// Set how input text triggers display of one or more predictions of the user's intended
/// value for a [`ComboBox`], [`SearchBox`], or [`TextInput`].
///
/// [`ComboBox`]: AccessRole::ComboBox
/// [`SearchBox`]: AccessRole::SearchBox
/// [`TextInput`]: AccessRole::TextInput
#[property(CONTEXT)]
pub fn auto_complete(child: impl UiNode, auto_complete: impl IntoVar<AutoComplete>) -> impl UiNode {
with_access_state(child, auto_complete, |b, v| b.set_auto_complete(*v))
}
/// If the widget is checked (`Some(true)`), unchecked (`Some(false)`), or if the checked status is indeterminate (`None`).
#[property(CONTEXT)]
pub fn checked(child: impl UiNode, checked: impl IntoVar<Option<bool>>) -> impl UiNode {
with_access_state(child, checked, |b, v| b.set_checked(*v))
}
/// Indicates that the widget represents the current item of a [kind](CurrentKind).
#[property(CONTEXT)]
pub fn current(child: impl UiNode, kind: impl IntoVar<CurrentKind>) -> impl UiNode {
with_access_state(child, kind, |b, v| b.set_current(*v))
}
/// Indicates that the widget is an error message for the `invalid_wgt`.
///
/// The other widget must [`invalid`].
///
/// [`invalid`]: fn@invalid
#[property(CONTEXT)]
pub fn error_message(child: impl UiNode, invalid_wgt: impl IntoVar<WidgetId>) -> impl UiNode {
with_access_state(child, invalid_wgt, |b, v| b.set_error_message(*v))
}
/// Identifies the currently active widget when focus is on a composite widget.
#[property(CONTEXT)]
pub fn active_descendant(child: impl UiNode, descendant: impl IntoVar<WidgetId>) -> impl UiNode {
with_access_state(child, descendant, |b, v| b.set_active_descendant(*v))
}
/// Indicate that the widget toggles the visibility of related widgets.
///
/// Use [`controls`], or [`owns`] to indicate the widgets that change visibility based on
/// this value.
///
/// [`controls`]: fn@controls
/// [`owns`]: fn@owns
#[property(CONTEXT)]
pub fn expanded(child: impl UiNode, expanded: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, expanded, |b, v| b.set_expanded(*v))
}
/// Indicates the availability and type of interactive popup widget.
#[property(CONTEXT)]
pub fn popup(child: impl UiNode, popup: impl IntoVar<Popup>) -> impl UiNode {
with_access_state(child, popup, |b, v| b.set_popup(*v))
}
/// Sets a custom name for the widget in accessibility info.
///
/// See also [`labelled_by`] and [`labelled_by_child`].
///
/// [`labelled_by`]: fn@labelled_by
/// [`labelled_by_child`]: fn@labelled_by_child
#[property(CONTEXT)]
pub fn label(child: impl UiNode, label: impl IntoVar<Txt>) -> impl UiNode {
with_access_state(child, label, |b, v| b.set_label(v.clone()))
}
/// Uses the accessible children as [`labelled_by`].
///
/// [`labelled_by`]: fn@labelled_by
#[property(CONTEXT)]
pub fn labelled_by_child(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, enabled, |b, v| {
if *v {
b.flag_labelled_by_child();
}
})
}
/// Sets the hierarchical level of the widget within a parent scope.
#[property(CONTEXT)]
pub fn level(child: impl UiNode, hierarchical_level: impl IntoVar<NonZeroU32>) -> impl UiNode {
with_access_state(child, hierarchical_level, |b, v| b.set_level(*v))
}
/// Indicates that the user may select more than one item from the current selectable descendants.
#[property(CONTEXT)]
pub fn multi_selectable(child: impl UiNode, multi_selectable: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, multi_selectable, |b, v| {
if *v {
b.flag_multi_selectable()
}
})
}
/// Indicates whether the widget's orientation is horizontal, vertical, or unknown/ambiguous.
#[property(CONTEXT)]
pub fn orientation(child: impl UiNode, orientation: impl IntoVar<Orientation>) -> impl UiNode {
with_access_state(child, orientation, |b, v| b.set_orientation(*v))
}
/// Short hint (a word or short phrase) intended to help the user with data entry when a form control has no value.
#[property(CONTEXT)]
pub fn placeholder(child: impl UiNode, placeholder: impl IntoVar<Txt>) -> impl UiNode {
with_access_state(child, placeholder, |b, v| b.set_placeholder(v.clone()))
}
/// Indicates that the widget is not editable, but is otherwise operable.
#[property(CONTEXT)]
pub fn read_only(child: impl UiNode, read_only: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, read_only, |b, v| {
if *v {
b.flag_read_only()
}
})
}
/// Indicates that user input is required on the widget before a form may be submitted.
#[property(CONTEXT)]
pub fn required(child: impl UiNode, required: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, required, |b, v| {
if *v {
b.flag_required()
}
})
}
/// Indicates that the widget is selected.
#[property(CONTEXT)]
pub fn selected(child: impl UiNode, selected: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, selected, |b, v| {
if *v {
b.flag_selected()
}
})
}
/// Sets the sort direction for the table or grid items.
#[property(CONTEXT)]
pub fn sort(child: impl UiNode, direction: impl IntoVar<SortDirection>) -> impl UiNode {
with_access_state(child, direction, |b, v| b.set_sort(*v))
}
/// Set the maximum value (inclusive).
#[property(CONTEXT)]
pub fn value_max(child: impl UiNode, max: impl IntoVar<f64>) -> impl UiNode {
with_access_state(child, max, |b, v| b.set_value_max(*v))
}
/// Set the minimum value (inclusive).
#[property(CONTEXT)]
pub fn value_min(child: impl UiNode, min: impl IntoVar<f64>) -> impl UiNode {
with_access_state(child, min, |b, v| b.set_value_min(*v))
}
/// Set the current value.
#[property(CONTEXT)]
pub fn value(child: impl UiNode, value: impl IntoVar<f64>) -> impl UiNode {
with_access_state(child, value, |b, v| b.set_value(*v))
}
/// Set a text that is a readable version of the current value.
#[property(CONTEXT)]
pub fn value_text(child: impl UiNode, value: impl IntoVar<Txt>) -> impl UiNode {
with_access_state(child, value, |b, v| b.set_value_text(v.clone()))
}
/// Sets the total number of columns in a [`Table`], [`Grid`], or [`TreeGrid`] when not all columns are present in tree.
///
/// The value `0` indicates that not all columns are in the widget and the application cannot determinate the exact number.
///
/// [`Table`]: AccessRole::Table
/// [`Grid`]: AccessRole::Grid
/// [`TreeGrid`]: AccessRole::TreeGrid
#[property(CONTEXT)]
pub fn col_count(child: impl UiNode, count: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, count, |b, v| b.set_col_count(*v))
}
/// Sets the widget's column index in the parent table or grid.
#[property(CONTEXT)]
pub fn col_index(child: impl UiNode, index: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, index, |b, v| b.set_col_index(*v))
}
/// Sets the number of columns spanned by the widget in the parent table or grid.
#[property(CONTEXT)]
pub fn col_span(child: impl UiNode, span: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, span, |b, v| b.set_col_span(*v))
}
/// Sets the total number of rows in a [`Table`], [`Grid`], or [`TreeGrid`] when not all rows are present in tree.
///
/// The value `0` indicates that not all rows are in the widget and the application cannot determinate the exact number.
///
/// [`Table`]: AccessRole::Table
/// [`Grid`]: AccessRole::Grid
/// [`TreeGrid`]: AccessRole::TreeGrid
#[property(CONTEXT)]
pub fn row_count(child: impl UiNode, count: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, count, |b, v| b.set_row_count(*v))
}
/// Sets the widget's row index in the parent table or grid.
#[property(CONTEXT)]
pub fn row_index(child: impl UiNode, index: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, index, |b, v| b.set_row_index(*v))
}
/// Sets the number of rows spanned by the widget in the parent table or grid.
#[property(CONTEXT)]
pub fn row_span(child: impl UiNode, span: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, span, |b, v| b.set_row_span(*v))
}
/// Sets the number of items in the current set of list items or tree items when not all items in the set are present in the tree.
#[property(CONTEXT)]
pub fn item_count(child: impl UiNode, count: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, count, |b, v| b.set_item_count(*v))
}
/// Sets the widget's number or position in the current set of list items or tree items when not all items are present in the tree.
#[property(CONTEXT)]
pub fn item_index(child: impl UiNode, index: impl IntoVar<usize>) -> impl UiNode {
with_access_state(child, index, |b, v| b.set_item_index(*v))
}
/// Sets if the widget is modal when displayed.
#[property(CONTEXT)]
pub fn modal(child: impl UiNode, modal: impl IntoVar<bool>) -> impl UiNode {
with_access_state(child, modal, |b, v| {
if *v {
b.flag_modal()
}
})
}
/// Append widgets whose contents or presence are controlled by this widget to the controlled list.
#[property(CONTEXT)]
pub fn controls(child: impl UiNode, controlled: impl IntoVar<Vec<WidgetId>>) -> impl UiNode {
with_access_state(child, controlled, |b, v| {
for id in v {
b.push_controls(*id);
}
})
}
/// Append widgets that describes this widget to the descriptors list.
#[property(CONTEXT)]
pub fn described_by(child: impl UiNode, descriptors: impl IntoVar<Vec<WidgetId>>) -> impl UiNode {
with_access_state(child, descriptors, |b, v| {
for id in v {
b.push_described_by(*id);
}
})
}
/// Append widgets that provide additional information related to this widget to the details list.
#[property(CONTEXT)]
pub fn details(child: impl UiNode, details: impl IntoVar<Vec<WidgetId>>) -> impl UiNode {
with_access_state(child, details, |b, v| {
for id in v {
b.push_details(*id);
}
})
}
/// Append widgets that provide additional information related to this widget.
#[property(CONTEXT)]
pub fn labelled_by(child: impl UiNode, labels: impl IntoVar<Vec<WidgetId>>) -> impl UiNode {
with_access_state(child, labels, |b, v| {
for id in v {
b.push_labelled_by(*id);
}
})
}
/// Append widgets that are a *child* of this widget, but is not already a child in the info tree.
#[property(CONTEXT)]
pub fn owns(child: impl UiNode, owned: impl IntoVar<Vec<WidgetId>>) -> impl UiNode {
with_access_state(child, owned, |b, v| {
for id in v {
b.push_owns(*id);
}
})
}
/// Append options for next widget to be read by screen readers.
#[property(CONTEXT)]
pub fn flows_to(child: impl UiNode, next_options: impl IntoVar<Vec<WidgetId>>) -> impl UiNode {
with_access_state(child, next_options, |b, v| {
for id in v {
b.push_flows_to(*id);
}
})
}
/// Indicates that the widget's data is invalid with optional kinds of errors.
#[property(CONTEXT)]
pub fn invalid(child: impl UiNode, error: impl IntoVar<Invalid>) -> impl UiNode {
with_access_state(child, error, |b, v| b.set_invalid(*v))
}
/// Defines the language used by screen-readers to read text in this widget and descendants.
#[property(CONTEXT)]
pub fn lang(child: impl UiNode, lang: impl IntoVar<Lang>) -> impl UiNode {
with_access_state(child, lang, |b, v| b.set_lang(v.0.clone()))
}
/// Sets the amount scrolled on the horizontal if the content can be scrolled horizontally.
///
/// The `normal_x` value can be a read-only variable, the variable can be updated without needing to rebuild
/// info for every pixel scrolled, if the view-process requires access info the value is updated every render
/// together with the widget bounds updates.
///
/// The value must be normalized in the 0..1 range, 0 is showing the content leftmost edge, 1 is showing
/// the content the rightmost edge.
#[property(CONTEXT)]
pub fn scroll_horizontal(child: impl UiNode, normal_x: impl IntoVar<Factor>) -> impl UiNode {
with_access_state_var(child, normal_x, |b, v| b.set_scroll_horizontal(v.clone()))
}
/// Sets the amount scrolled on the vertical if the content can be scrolled vertically.
///
/// The `normal_y` value can be a read-only variable, the variable can be updated without needing to rebuild
/// info for every pixel scrolled, if the view-process requires access info the value is updated every render
/// together with the widget bounds updates.
///
/// The value must be normalized in the 0..1 range, 0 is showing the content topmost edge, 1 is showing
/// the content the bottommost edge.
#[property(CONTEXT)]
pub fn scroll_vertical(child: impl UiNode, normal_y: impl IntoVar<Factor>) -> impl UiNode {
with_access_state_var(child, normal_y, |b, v| b.set_scroll_vertical(v.clone()))
}
/// Indicate that the widget can change, how the change can be announced, if `atomic`
/// the entire widget must be re-read, if `busy` the screen reader must wait until the change completes.
#[property(CONTEXT)]
pub fn live(
child: impl UiNode,
indicator: impl IntoVar<LiveIndicator>,
atomic: impl IntoVar<bool>,
busy: impl IntoVar<bool>,
) -> impl UiNode {
let indicator = indicator.into_var();
let atomic = atomic.into_var();
let busy = busy.into_var();
let mut handles = VarHandles::dummy();
match_node(child, move |c, op| match op {
UiNodeOp::Deinit => {
handles.clear();
}
UiNodeOp::Info { info } => {
c.info(info);
if let Some(mut builder) = info.access() {
if handles.is_dummy() {
handles.push(indicator.subscribe(UpdateOp::Info, WIDGET.id()));
handles.push(atomic.subscribe(UpdateOp::Info, WIDGET.id()));
handles.push(busy.subscribe(UpdateOp::Info, WIDGET.id()));
}
builder.set_live(indicator.get(), atomic.get(), busy.get());
}
}
_ => {}
})
}
fn with_access_state<T: VarValue>(
child: impl UiNode,
state: impl IntoVar<T>,
set_info: impl Fn(&mut WidgetAccessInfoBuilder, &T) + Send + 'static,
) -> impl UiNode {
with_access_state_var(child, state, move |b, v| v.with(|v| set_info(b, v)))
}
fn with_access_state_var<T: VarValue, I: IntoVar<T>>(
child: impl UiNode,
state: I,
set_info: impl Fn(&mut WidgetAccessInfoBuilder, &I::Var) + Send + 'static,
) -> impl UiNode {
let state = state.into_var();
let mut handle = VarHandle::dummy();
match_node(child, move |c, op| match op {
UiNodeOp::Deinit => {
handle = VarHandle::dummy();
}
UiNodeOp::Info { info } => {
c.info(info);
if let Some(mut builder) = info.access() {
if handle.is_dummy() {
handle = state.subscribe(UpdateOp::Info, WIDGET.id());
}
set_info(&mut builder, &state)
}
}
_ => {}
})
}

View File

@ -0,0 +1,15 @@
[package]
name = "zero-ui-wgt-button"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-wgt-container = { path = "../zero-ui-wgt-container" }
zero-ui-wgt-style = { path = "../zero-ui-wgt-style" }
zero-ui-wgt-input = { path = "../zero-ui-wgt-input" }
zero-ui-wgt-access = { path = "../zero-ui-wgt-access" }
zero-ui-wgt-fill = { path = "../zero-ui-wgt-fill" }
zero-ui-wgt-filters = { path = "../zero-ui-wgt-filters" }

View File

@ -0,0 +1,172 @@
//! Button widget.
use zero_ui_wgt::{border, corner_radius, is_disabled, prelude::*, InteractivityMix};
use zero_ui_wgt_access::{access_role, labelled_by_child, AccessRole};
use zero_ui_wgt_container::{child_align, padding, Container};
use zero_ui_wgt_fill::background_color;
use zero_ui_wgt_input::{
capture_pointer, cursor,
focus::FocusableMix,
gesture::{on_click, ClickArgs},
is_cap_hovered, is_pressed, CaptureMode, CursorIcon,
};
use zero_ui_wgt_style::{Style, StyleFn, StyleMix};
use zero_ui_wgt_filters::{saturate, child_opacity};
/// A clickable container.
#[widget($crate::Button)]
pub struct Button(FocusableMix<StyleMix<InteractivityMix<Container>>>);
impl Button {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
style_fn = STYLE_VAR;
capture_pointer = true;
labelled_by_child = true;
}
}
widget_impl! {
/// Button click event.
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// #
/// # Button! {
/// on_click = hn!(|args: &ClickArgs| {
/// assert!(args.is_primary());
/// println!("button {:?} clicked!", WIDGET.id());
/// });
/// child = Text!("Click Me!");
/// # }
/// # ;
/// ```
pub on_click(handler: impl WidgetHandler<ClickArgs>);
/// If pointer interaction with other widgets is blocked while the button is pressed.
///
/// Enabled by default in this widget.
pub capture_pointer(mode: impl IntoVar<CaptureMode>);
}
}
context_var! {
/// Button style in a context.
///
/// Is the [`DefaultStyle!`] by default.
///
/// [`DefaultStyle!`]: struct@DefaultStyle
pub static STYLE_VAR: StyleFn = StyleFn::new(|_| DefaultStyle!());
/// Idle background dark and light color.
pub static BASE_COLORS_VAR: ColorPair = (rgb(0.18, 0.18, 0.18), rgb(0.82, 0.82, 0.82));
}
/// Sets the [`BASE_COLORS_VAR`] that is used to compute all background and border colors in the button style.
#[property(CONTEXT, default(BASE_COLORS_VAR), widget_impl(DefaultStyle))]
pub fn base_colors(child: impl UiNode, color: impl IntoVar<ColorPair>) -> impl UiNode {
with_context_var(child, BASE_COLORS_VAR, color)
}
/// Sets the button style in a context, the parent style is fully replaced.
#[property(CONTEXT, default(STYLE_VAR))]
pub fn replace_style(child: impl UiNode, style: impl IntoVar<StyleFn>) -> impl UiNode {
with_context_var(child, STYLE_VAR, style)
}
/// Extends the button style in a context, the parent style is used, properties of the same name set in
/// `style` override the parent style.
#[property(CONTEXT, default(StyleFn::nil()))]
pub fn extend_style(child: impl UiNode, style: impl IntoVar<StyleFn>) -> impl UiNode {
zero_ui_wgt_style::with_style_extension(child, STYLE_VAR, style)
}
/// Create a [`color_scheme_highlight`] of `0.08`.
pub fn color_scheme_hovered(pair: impl IntoVar<ColorPair>) -> impl Var<Rgba> {
color_scheme_highlight(pair, 0.08)
}
/// Create a [`color_scheme_highlight`] of `0.16`.
pub fn color_scheme_pressed(pair: impl IntoVar<ColorPair>) -> impl Var<Rgba> {
color_scheme_highlight(pair, 0.16)
}
/// Button default style.
#[widget($crate::DefaultStyle)]
pub struct DefaultStyle(Style);
impl DefaultStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
access_role = AccessRole::Button;
padding = (7, 15);
corner_radius = 4;
child_align = Align::CENTER;
#[easing(150.ms())]
background_color = color_scheme_pair(BASE_COLORS_VAR);
#[easing(150.ms())]
border = {
widths: 1,
sides: color_scheme_pair(BASE_COLORS_VAR).map_into()
};
when *#is_cap_hovered {
#[easing(0.ms())]
background_color = color_scheme_hovered(BASE_COLORS_VAR);
#[easing(0.ms())]
border = {
widths: 1,
sides: color_scheme_pressed(BASE_COLORS_VAR).map_into(),
};
}
when *#is_pressed {
#[easing(0.ms())]
background_color = color_scheme_pressed(BASE_COLORS_VAR);
}
when *#is_disabled {
saturate = false;
child_opacity = 50.pct();
cursor = CursorIcon::NotAllowed;
}
}
}
}
/// Button link style.
///
/// Looks like a web hyperlink.
#[widget($crate::LinkStyle)]
pub struct LinkStyle(Style);
impl LinkStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
text::font_color = color_scheme_map(web_colors::LIGHT_BLUE, colors::BLUE);
cursor = CursorIcon::Pointer;
access_role = AccessRole::Link;
when *#is_cap_hovered {
text::underline = 1, LineStyle::Solid;
}
when *#is_pressed {
text::font_color = color_scheme_map(colors::YELLOW, web_colors::BROWN);
}
when *#is_disabled {
saturate = false;
child_opacity = 50.pct();
cursor = CursorIcon::NotAllowed;
}
}
}
}

View File

@ -0,0 +1,12 @@
[package]
name = "zero-ui-wgt-container"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-app = { path = "../zero-ui-app" }
serde = { version = "1", features = ["derive"] }

View File

@ -0,0 +1,416 @@
//! Single child container base.
use std::fmt;
use zero_ui_wgt::{align, clip_to_bounds, margin, prelude::*, Wgt};
/// Base single content container.
#[widget($crate::Container {
($child:expr) => {
child = $child;
}
})]
pub struct Container(Wgt);
impl Container {
fn widget_intrinsic(&mut self) {
self.widget_builder().push_build_action(|wgt| {
if let Some(child) = wgt.capture_ui_node(property_id!(Self::child)) {
wgt.set_child(child);
}
});
}
widget_impl! {
/// The content.
///
/// Can be any type that implements [`UiNode`], any widget.
///
/// [`UiNode`]: crate::core::widget_instance::UiNode
pub zero_ui_app::widget::base::child(child: impl UiNode);
/// Content overflow clipping.
pub clip_to_bounds(clip: impl IntoVar<bool>);
}
}
/// Margin space around the *content* of a widget.
///
/// This property is [`margin`](fn@margin) with nest group `CHILD_LAYOUT`.
#[property(CHILD_LAYOUT, default(0), widget_impl(Container))]
pub fn padding(child: impl UiNode, padding: impl IntoVar<SideOffsets>) -> impl UiNode {
margin(child, padding)
}
/// Aligns the widget *content* within the available space.
///
/// This property is [`align`](fn@align) with nest group `CHILD_LAYOUT`.
#[property(CHILD_LAYOUT, default(Align::FILL), widget_impl(Container))]
pub fn child_align(child: impl UiNode, alignment: impl IntoVar<Align>) -> impl UiNode {
align(child, alignment)
}
/// Placement of a node inserted by the [`child_insert`] property.
///
/// [`child_insert`]: fn@child_insert
#[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ChildInsertPlace {
/// Insert node above the child.
Above,
/// Insert node to the right of child.
Right,
/// Insert node below the child.
Below,
/// Insert node to the left of child.
Left,
/// Insert node to the left of child in [`LayoutDirection::LTR`] contexts and to the right of child
/// in [`LayoutDirection::RTL`] contexts.
Start,
/// Insert node to the right of child in [`LayoutDirection::LTR`] contexts and to the left of child
/// in [`LayoutDirection::RTL`] contexts.
End,
}
impl fmt::Debug for ChildInsertPlace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "ChildInsertPlace::")?;
}
match self {
Self::Above => write!(f, "Above"),
Self::Right => write!(f, "Right"),
Self::Below => write!(f, "Below"),
Self::Left => write!(f, "Left"),
Self::Start => write!(f, "Start"),
Self::End => write!(f, "End"),
}
}
}
impl ChildInsertPlace {
/// Convert [`ChildInsertPlace::Start`] and [`ChildInsertPlace::End`] to the fixed place they represent in the `direction` context.
pub fn resolve_direction(self, direction: LayoutDirection) -> Self {
match self {
Self::Start => match direction {
LayoutDirection::LTR => Self::Left,
LayoutDirection::RTL => Self::Right,
},
Self::End => match direction {
LayoutDirection::LTR => Self::Right,
LayoutDirection::RTL => Self::Left,
},
p => p,
}
}
/// Inserted node is to the left or right of child.
pub fn is_x_axis(self) -> bool {
!matches!(self, Self::Above | Self::Below)
}
/// Inserted node is above or bellow the child node.
pub fn is_y_axis(self) -> bool {
matches!(self, Self::Above | Self::Below)
}
}
/// Insert the `insert` node in the `place` relative to the widget's child.
///
/// This property disables inline layout for the widget.
#[property(CHILD, default(ChildInsertPlace::Start, NilUiNode, 0), widget_impl(Container))]
pub fn child_insert(
child: impl UiNode,
place: impl IntoVar<ChildInsertPlace>,
insert: impl UiNode,
spacing: impl IntoVar<Length>,
) -> impl UiNode {
let place = place.into_var();
let spacing = spacing.into_var();
let offset_key = FrameValueKey::new_unique();
let mut offset_child = 0;
let mut offset = PxVector::zero();
match_node_list(ui_vec![child, insert], move |children, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&place).sub_var_layout(&spacing);
}
UiNodeOp::Measure { wm, desired_size } => {
let c = LAYOUT.constraints();
*desired_size = if place.get().is_x_axis() {
let mut spacing = spacing.layout_x();
let insert_size = children.with_node(1, |n| {
LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_x(false), || wm.measure_block(n))
});
if insert_size.width == Px(0) {
spacing = Px(0);
}
let child_size = children.with_node(0, |n| {
LAYOUT.with_constraints(c.with_less_x(insert_size.width + spacing), || wm.measure_block(n))
});
PxSize::new(
insert_size.width + spacing + child_size.width,
insert_size.height.max(child_size.height),
)
} else {
let mut spacing = spacing.layout_y();
let insert_size = children.with_node(1, |n| {
LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_y(false), || wm.measure_block(n))
});
if insert_size.height == Px(0) {
spacing = Px(0);
}
let child_size = children.with_node(0, |n| {
LAYOUT.with_constraints(c.with_less_y(insert_size.height + spacing), || wm.measure_block(n))
});
if child_size.height == Px(0) {
spacing = Px(0);
}
PxSize::new(
insert_size.width.max(child_size.width),
insert_size.height + spacing + child_size.height,
)
};
}
UiNodeOp::Layout { wl, final_size } => {
let place = place.get().resolve_direction(LAYOUT.direction());
let c = LAYOUT.constraints();
*final_size = match place {
ChildInsertPlace::Left | ChildInsertPlace::Right => {
let spacing = spacing.layout_x();
let mut constraints_y = LAYOUT.constraints().y;
if constraints_y.fill_or_exact().is_none() {
// measure to find fill height
let mut wm = wl.to_measure(None);
let wm = &mut wm;
let mut spacing = spacing;
let insert_size = children.with_node(1, |n| {
LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_x(false), || n.measure(wm))
});
if insert_size.width == Px(0) {
spacing = Px(0);
}
let child_size = children.with_node(0, |n| {
LAYOUT.with_constraints(c.with_less_x(insert_size.width + spacing), || n.measure(wm))
});
constraints_y = constraints_y.with_fill(true).with_max(child_size.height.max(insert_size.height));
}
let mut spacing = spacing;
let insert_size = children.with_node(1, |n| {
LAYOUT.with_constraints(
{
let mut c = c;
c.y = constraints_y;
c.with_new_min(Px(0), Px(0)).with_fill_x(false)
},
|| n.layout(wl),
)
});
if insert_size.width == Px(0) {
spacing = Px(0);
}
let child_size = children.with_node(0, |n| {
LAYOUT.with_constraints(
{
let mut c = c;
c.y = constraints_y;
c.with_less_x(insert_size.width + spacing)
},
|| n.layout(wl),
)
});
if child_size.width == Px(0) {
spacing = Px(0);
}
// position
let (child, o) = match place {
ChildInsertPlace::Left => (0, insert_size.width + spacing),
ChildInsertPlace::Right => (1, child_size.width + spacing),
_ => unreachable!(),
};
let o = PxVector::new(o, Px(0));
if offset != o || offset_child != child {
offset_child = child;
offset = o;
WIDGET.render_update();
}
PxSize::new(
insert_size.width + spacing + child_size.width,
insert_size.height.max(child_size.height),
)
}
ChildInsertPlace::Above | ChildInsertPlace::Below => {
let spacing = spacing.layout_y();
let mut constraints_x = c.x;
if constraints_x.fill_or_exact().is_none() {
// measure fill width
let mut wm = wl.to_measure(None);
let wm = &mut wm;
let mut spacing = spacing;
let insert_size = children.with_node(1, |n| {
LAYOUT.with_constraints(c.with_new_min(Px(0), Px(0)).with_fill_y(false), || n.measure(wm))
});
if insert_size.height == Px(0) {
spacing = Px(0);
}
let child_size = children.with_node(0, |n| {
LAYOUT.with_constraints(c.with_less_y(insert_size.height + spacing), || n.measure(wm))
});
constraints_x = constraints_x.with_fill(true).with_max(child_size.width.max(insert_size.width));
}
let mut spacing = spacing;
let insert_size = children.with_node(1, |n| {
LAYOUT.with_constraints(
{
let mut c = c;
c.x = constraints_x;
c.with_new_min(Px(0), Px(0)).with_fill_y(false)
},
|| n.layout(wl),
)
});
if insert_size.height == Px(0) {
spacing = Px(0);
}
let child_size = children.with_node(0, |n| {
LAYOUT.with_constraints(
{
let mut c = c;
c.x = constraints_x;
c.with_less_y(insert_size.height + spacing)
},
|| n.layout(wl),
)
});
// position
let (child, o) = match place {
ChildInsertPlace::Above => (0, insert_size.height + spacing),
ChildInsertPlace::Below => (1, child_size.height + spacing),
_ => unreachable!(),
};
let o = PxVector::new(Px(0), o);
if offset != o || offset_child != child {
offset_child = child;
offset = o;
WIDGET.render_update();
}
PxSize::new(
insert_size.width.max(child_size.width),
insert_size.height + spacing + child_size.height,
)
}
_ => {
unreachable!()
}
};
}
UiNodeOp::Render { frame } => children.for_each(|i, child| {
if i as u8 == offset_child {
frame.push_reference_frame(offset_key.into(), offset_key.bind(offset.into(), false), true, true, |frame| {
child.render(frame);
});
} else {
child.render(frame);
}
}),
UiNodeOp::RenderUpdate { update } => {
children.for_each(|i, child| {
if i as u8 == offset_child {
update.with_transform(offset_key.update(offset.into(), false), true, |update| {
child.render_update(update);
});
} else {
child.render_update(update);
}
});
}
_ => {}
})
}
/// Insert the `insert` node in the `place` relative to the widget's child, but outside of the child layout.
///
/// This is still *inside* the parent widget, but outside of properties like padding.
///
/// This property disables inline layout for the widget.
#[property(CHILD_LAYOUT - 1, default(ChildInsertPlace::Start, NilUiNode, 0), widget_impl(Container))]
pub fn child_out_insert(
child: impl UiNode,
place: impl IntoVar<ChildInsertPlace>,
insert: impl UiNode,
spacing: impl IntoVar<Length>,
) -> impl UiNode {
child_insert(child, place, insert, spacing)
}
/// Insert `insert` to the left of the widget's child.
///
/// This property disables inline layout for the widget. See [`child_insert`] for more details.
///
/// [`child_insert`]: fn@child_insert
#[property(CHILD, default(NilUiNode, 0), widget_impl(Container))]
pub fn child_insert_left(child: impl UiNode, insert: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
child_insert(child, ChildInsertPlace::Left, insert, spacing)
}
/// Insert `insert` to the right of the widget's child.
///
/// This property disables inline layout for the widget. See [`child_insert`] for more details.
///
/// [`child_insert`]: fn@child_insert
#[property(CHILD, default(NilUiNode, 0), widget_impl(Container))]
pub fn child_insert_right(child: impl UiNode, insert: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
child_insert(child, ChildInsertPlace::Right, insert, spacing)
}
/// Insert `insert` above the widget's child.
///
/// This property disables inline layout for the widget. See [`child_insert`] for more details.
///
/// [`child_insert`]: fn@child_insert
#[property(CHILD, default(NilUiNode, 0), widget_impl(Container))]
pub fn child_insert_above(child: impl UiNode, insert: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
child_insert(child, ChildInsertPlace::Above, insert, spacing)
}
/// Insert `insert` below the widget's child.
///
/// This property disables inline layout for the widget. See [`child_insert`] for more details.
///
/// [`child_insert`]: fn@child_insert
#[property(CHILD, default(NilUiNode, 0), widget_impl(Container))]
pub fn child_insert_below(child: impl UiNode, insert: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
child_insert(child, ChildInsertPlace::Below, insert, spacing)
}
/// Insert `insert` to the left of the widget's child in LTR contexts or to the right of the widget's child in RTL contexts.
///
/// This property disables inline layout for the widget. See [`child_insert`] for more details.
///
/// [`child_insert`]: fn@child_insert
#[property(CHILD, default(NilUiNode, 0), widget_impl(Container))]
pub fn child_insert_start(child: impl UiNode, insert: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
child_insert(child, ChildInsertPlace::Start, insert, spacing)
}
/// Insert `insert` to the right of the widget's child in LTR contexts or to the right of the widget's child in RTL contexts.
///
/// This property disables inline layout for the widget. See [`child_insert`] for more details.
///
/// [`child_insert`]: fn@child_insert
#[property(CHILD, default(NilUiNode, 0), widget_impl(Container))]
pub fn child_insert_end(child: impl UiNode, insert: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
child_insert(child, ChildInsertPlace::End, insert, spacing)
}

View File

@ -0,0 +1,13 @@
[package]
name = "zero-ui-wgt-data"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-color = { path = "../zero-ui-color" }
zero-ui-var = { path = "../zero-ui-var" }
serde = { version = "1", features = ["derive"] }

687
zero-ui-wgt-data/src/lib.rs Normal file
View File

@ -0,0 +1,687 @@
//! Contextual [`DATA`] and validation.
use std::{any::Any, collections::HashMap, fmt, mem, num::NonZeroU8, ops, sync::Arc};
use zero_ui_color::COLOR_SCHEME_VAR;
use zero_ui_var::{types::ContextualizedVar, BoxedAnyVar};
use zero_ui_wgt::prelude::*;
use task::parking_lot::RwLock;
/// Data context.
///
/// Sets the [`DATA`] context for this widget and descendants, replacing the parent's data. Note that only
/// one data context can be set at a time, the `data` will override the parent's data even if the type `T`
/// does not match.
#[property(CONTEXT - 1)]
pub fn data<T: VarValue>(child: impl UiNode, data: impl IntoVar<T>) -> impl UiNode {
with_context_local(child, &DATA_CTX, data.into_var().boxed_any())
}
/// Insert a data note in the context.
///
/// This properties synchronizes the `level` and `note` variables with an [`DATA.annotate`] entry. If
/// the `note` is empty the data note is not inserted.
///
/// [`DATA.annotate`]: DATA::annotate
#[property(CONTEXT, default(DataNoteLevel::INFO, ""))]
pub fn data_note(child: impl UiNode, level: impl IntoVar<DataNoteLevel>, note: impl IntoVar<Txt>) -> impl UiNode {
let level = level.into_var();
let note = note.into_var();
let mut _handle = DataNoteHandle::dummy();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&level).sub_var(&note);
let note = note.get();
if !note.is_empty() {
_handle = DATA.annotate(level.get(), note);
}
}
UiNodeOp::Deinit => {
_handle = DataNoteHandle::dummy();
}
UiNodeOp::Update { .. } => {
if level.is_new() || note.is_new() {
let note = note.get();
_handle = if note.is_empty() {
DataNoteHandle::dummy()
} else {
DATA.annotate(level.get(), note)
};
}
}
_ => {}
})
}
/// Insert a data INFO note in the context.
///
/// This properties synchronizes the `note` variable with an [`DATA.inform`] entry. If
/// the `note` is empty the data note is not inserted.
///
/// [`DATA.inform`]: DATA::inform
#[property(CONTEXT, default(""))]
pub fn data_info(child: impl UiNode, note: impl IntoVar<Txt>) -> impl UiNode {
data_note(child, DataNoteLevel::INFO, note)
}
/// Insert a data WARN note in the context.
///
/// This properties synchronizes the `note` variable with an [`DATA.warn`] entry. If
/// the `note` is empty the data note is not inserted.
///
/// [`DATA.warn`]: DATA::warn
#[property(CONTEXT, default(""))]
pub fn data_warn(child: impl UiNode, note: impl IntoVar<Txt>) -> impl UiNode {
data_note(child, DataNoteLevel::WARN, note)
}
/// Insert a data ERROR note in the context.
///
/// This properties synchronizes the `note` variable with an [`DATA.invalidate`] entry. If
/// the `note` is empty the data note is not inserted.
///
/// [`DATA.invalidate`]: DATA::invalidate
#[property(CONTEXT, default(""))]
pub fn data_error(child: impl UiNode, note: impl IntoVar<Txt>) -> impl UiNode {
data_note(child, DataNoteLevel::ERROR, note)
}
/// Get all data notes set on the context.
#[property(CONTEXT - 1)]
pub fn get_data_notes(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.clone());
})
}
/// Gets if any data notes are set on the context.
#[property(CONTEXT - 1)]
pub fn has_data_notes(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
let any = any.into_var();
with_data_notes(child, move |n| {
let _ = any.set(!n.is_empty());
})
}
/// Get all `INFO` data notes set on the context.
#[property(CONTEXT - 1)]
pub fn get_data_info(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.clone_level(DataNoteLevel::INFO));
})
}
/// Write all `INFO` data notes set on the context to a text.
#[property(CONTEXT - 1)]
pub fn get_data_info_txt(child: impl UiNode, notes: impl IntoVar<Txt>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.level_txt(DataNoteLevel::INFO));
})
}
/// Gets if any `INFO` data notes are set on the context.
#[property(CONTEXT - 1)]
pub fn has_data_info(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
let any = any.into_var();
with_data_notes(child, move |n| {
let _ = any.set(n.iter().any(|n| n.level() == DataNoteLevel::INFO));
})
}
/// Get all `WARN` data notes set on the context.
#[property(CONTEXT - 1)]
pub fn get_data_warn(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.clone_level(DataNoteLevel::WARN));
})
}
/// Write all `WARN` data notes set on the context to a text.
#[property(CONTEXT - 1)]
pub fn get_data_warn_txt(child: impl UiNode, notes: impl IntoVar<Txt>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.level_txt(DataNoteLevel::WARN));
})
}
/// Gets if any `WARN` data notes are set on the context.
#[property(CONTEXT - 1)]
pub fn has_data_warn(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
let any = any.into_var();
with_data_notes(child, move |n| {
let _ = any.set(n.iter().any(|n| n.level() == DataNoteLevel::WARN));
})
}
/// Get all `ERROR` data notes set on the context.
#[property(CONTEXT - 1)]
pub fn get_data_error(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.clone_level(DataNoteLevel::ERROR));
})
}
/// Write all `ERROR` data notes set on the context to a text.
#[property(CONTEXT - 1)]
pub fn get_data_error_txt(child: impl UiNode, notes: impl IntoVar<Txt>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(n.level_txt(DataNoteLevel::ERROR));
})
}
/// Gets if any `ERROR` data notes are set on the context.
#[property(CONTEXT - 1)]
pub fn has_data_error(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
let any = any.into_var();
with_data_notes(child, move |n| {
let _ = any.set(n.iter().any(|n| n.level() == DataNoteLevel::ERROR));
})
}
/// Gets all the notes of highest data level set on the context.
#[property(CONTEXT - 1)]
pub fn get_data_notes_top(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
let notes = notes.into_var();
with_data_notes(child, move |n| {
let _ = notes.set(if let Some(top) = n.iter().map(|n| n.level()).max() {
n.clone_level(top)
} else {
DataNotes::default()
});
})
}
context_var! {
/// Color pairs for note levels.
///
/// The colors can be used directly as text color.
///
/// Defaults set only for the named levels.
pub static DATA_NOTE_COLORS_VAR: HashMap<DataNoteLevel, ColorPair> = {
let mut map = HashMap::new();
// (dark, light)
map.insert(DataNoteLevel::INFO, (colors::AZURE, colors::AZURE).into());
map.insert(DataNoteLevel::WARN, (colors::YELLOW, colors::ORANGE).into());
map.insert(DataNoteLevel::ERROR, (colors::WHITE.with_alpha(20.pct()).mix_normal(colors::RED), colors::RED).into());
map
};
}
/// Sets the data note level colors, the parent colors are fully replaced.
///
/// The colors will be used directly as text color or other bright *foreground* icon.
///
/// This property sets the [`DATA_NOTE_COLORS_VAR`].
#[property(CONTEXT, default(DATA_NOTE_COLORS_VAR))]
pub fn replace_data_note_colors(child: impl UiNode, colors: impl IntoVar<HashMap<DataNoteLevel, ColorPair>>) -> impl UiNode {
with_context_var(child, DATA_NOTE_COLORS_VAR, colors)
}
/// Extend the data note level colors, the `colors` extend the parent colors, only entries of the same level are replaced.
///
/// The colors will be used directly as text color or other bright *foreground* icon.
///
/// This property sets the [`DATA_NOTE_COLORS_VAR`].
#[property(CONTEXT, default(HashMap::new()))]
pub fn extend_data_note_colors(child: impl UiNode, colors: impl IntoVar<HashMap<DataNoteLevel, ColorPair>>) -> impl UiNode {
with_context_var(
child,
DATA_NOTE_COLORS_VAR,
merge_var!(DATA_NOTE_COLORS_VAR, colors.into_var(), |base, over| {
let mut base = base.clone();
base.extend(over);
base
}),
)
}
/// Node that inserts a data note color in [`DATA_NOTE_COLORS_VAR`].
pub fn with_data_note_color(child: impl UiNode, level: DataNoteLevel, color: impl IntoVar<ColorPair>) -> impl UiNode {
with_context_var(
child,
DATA_NOTE_COLORS_VAR,
merge_var!(DATA_NOTE_COLORS_VAR, color.into_var(), move |base, over| {
let mut base = base.clone();
base.insert(level, *over);
base
}),
)
}
/// Set the data note `INFO` color.
///
/// The color will be used directly as text color or other bright *foreground* icon.
#[property(CONTEXT)]
pub fn data_info_color(child: impl UiNode, color: impl IntoVar<ColorPair>) -> impl UiNode {
with_data_note_color(child, DataNoteLevel::INFO, color)
}
/// Set the data note `WARN` color.
///
/// The color will be used directly as text color or other bright *foreground* icon.
#[property(CONTEXT)]
pub fn data_warn_color(child: impl UiNode, color: impl IntoVar<ColorPair>) -> impl UiNode {
with_data_note_color(child, DataNoteLevel::WARN, color)
}
/// Set the data note `ERROR` color.
///
/// The color will be used directly as text color or other bright *foreground* icon.
#[property(CONTEXT)]
pub fn data_error_color(child: impl UiNode, color: impl IntoVar<ColorPair>) -> impl UiNode {
with_data_note_color(child, DataNoteLevel::ERROR, color)
}
/// Data context and validation.
///
/// This service enables data flow from a context to descendants, a little like an anonymous context var, and
/// from descendants up-to contexts.
///
/// Arbitrary data can be set on a context using the [`data`] property and retrieved using [`DATA.get`] or [`DATA.req`],
/// behaving a little like an anonymous context var. Only one data entry and type can exist in a context, nested
/// [`data`] properties override the parent data and type in their context.
///
/// Annotation on the data can be set back using [`DATA.annotate`] and can be retrieved using the [`get_data_notes`] property,
/// annotations are classified by [`DataNoteLevel`], including `INFO`, `WARN` and `ERROR`. For each level there are specialized
/// methods and properties, as an example, the [`DATA.invalidate`] is used to set an error note, and the [`get_data_error_txt`]
/// property gets the error formatted for display. Data notes are aggregated from descendants up-to the context, continuing
/// up to outer nested contexts too, this means that you can get data errors for a form field by setting [`get_data_error_txt`] on
/// the field widget, and get all form errors from that field and others by also setting [`get_data_error_txt`] in the form widget.
///
/// [`data`]: fn@data
/// [`get_data_notes`]: fn@get_data_notes
/// [`get_data_error_txt`]: fn@get_data_error_txt
/// [`DATA.get`]: DATA::get
/// [`DATA.req`]: DATA::req
/// [`DATA.annotate`]: DATA::annotate
/// [`DATA.invalidate`]: DATA::invalidate
pub struct DATA;
impl DATA {
/// Require context data of type `T`.
///
/// # Panics
///
/// Panics if the context data is not set to a variable of type `T` on the first usage of the returned variable.
pub fn req<T: VarValue>(&self) -> ContextualizedVar<T, BoxedVar<T>> {
self.get(|| panic!("expected DATA of type `{}`", std::any::type_name::<T>()))
}
/// Get context data of type `T` if the context data is set with the same type, or gets the `fallback` value.
pub fn get<T: VarValue>(&self, fallback: impl Fn() -> T + Send + Sync + 'static) -> ContextualizedVar<T, BoxedVar<T>> {
ContextualizedVar::new(Arc::new(move || {
DATA_CTX
.get()
.clone_any()
.double_boxed_any()
.downcast::<BoxedVar<T>>()
.map(|b| *b)
.unwrap_or_else(|_| LocalVar(fallback()).boxed())
}))
}
/// Gets the current context data.
///
/// Note that this is does not return a contextualizing var like [`get`], it gets the data var in the calling context.
///
/// [`get`]: Self::get
pub fn get_any(&self) -> BoxedAnyVar {
DATA_CTX.get().clone_any()
}
/// Insert a data note in the current context.
///
/// The note will stay in context until the context is unloaded or the handle is dropped.
pub fn annotate(&self, level: DataNoteLevel, note: impl DataNoteValue) -> DataNoteHandle {
if !DATA_NOTES_CTX.is_default() {
let (note, handle) = DataNote::new(WIDGET.id(), level, note);
let notes = DATA_NOTES_CTX.get();
let mut notes = notes.write();
notes.notes.notes.push(note);
notes.changed = true;
handle
} else {
DataNoteHandle::dummy()
}
}
/// Insert an `INFO` note in the current context.
///
/// The note will stay in context until the context is unloaded or the handle is dropped.
pub fn inform(&self, note: impl DataNoteValue) -> DataNoteHandle {
self.annotate(DataNoteLevel::INFO, note)
}
/// Insert a `WARN` note in the current context.
///
/// The note will stay in context until the context is unloaded or the handle is dropped.
pub fn warn(&self, note: impl DataNoteValue) -> DataNoteHandle {
self.annotate(DataNoteLevel::WARN, note)
}
/// Insert an `ERROR` note in the current context.
///
/// The note will stay in context until the context is unloaded or the handle is dropped.
pub fn invalidate(&self, note: impl DataNoteValue) -> DataNoteHandle {
self.annotate(DataNoteLevel::ERROR, note)
}
/// Read-only variable that is the best color for the note level in the context of the current color scheme.
///
/// If the `level` is not found, gets the nearest less than level, if no color is set in the context gets
/// the black/white for dark/light.
///
/// The color can be used directly as text color, it probably needs mixing or desaturating to use as background.
pub fn note_color(&self, level: impl IntoVar<DataNoteLevel>) -> impl Var<Rgba> {
merge_var!(DATA_NOTE_COLORS_VAR, level.into_var(), COLOR_SCHEME_VAR, |map, level, scheme| {
let c = if let Some(c) = map.get(level) {
*c
} else {
let mut nearest = 0u8;
let mut color = None;
for (l, c) in map {
if l.0.get() < level.0.get() && l.0.get() > nearest {
nearest = l.0.get();
color = Some(*c);
}
}
color.unwrap_or_else(|| ColorPair::from((colors::BLACK, colors::WHITE)))
};
match scheme {
ColorScheme::Light => c.light,
ColorScheme::Dark => c.dark,
}
})
}
/// Read-only variable that is the best color for `INFO` notes in the context of the current color scheme.
///
/// The color can be used directly as text color, it probably needs mixing or desaturating to use as background.
pub fn info_color(&self) -> impl Var<Rgba> {
self.note_color(DataNoteLevel::INFO)
}
/// Read-only variable that is the best color for `WARN` notes in the context of the current color scheme.
///
/// The color can be used directly as text color, it probably needs mixing or desaturating to use as background.
pub fn warn_color(&self) -> impl Var<Rgba> {
self.note_color(DataNoteLevel::WARN)
}
/// Read-only variable that is the best color for `ERROR` notes in the context of the current color scheme.
///
/// The color can be used directly as text color, it probably needs mixing or desaturating to use as background.
pub fn error_color(&self) -> impl Var<Rgba> {
self.note_color(DataNoteLevel::ERROR)
}
}
context_local! {
static DATA_CTX: BoxedAnyVar = LocalVar(()).boxed_any();
static DATA_NOTES_CTX: RwLock<DataNotesProbe> = RwLock::default();
}
/// Classifies the kind of information conveyed by a [`DataNote`].
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct DataNoteLevel(pub NonZeroU8);
impl DataNoteLevel {
// SAFETY: values are not zero.
/// Entry represents useful information.
pub const INFO: Self = Self(unsafe { NonZeroU8::new_unchecked(1) });
/// Entry represents a data validation warning.
pub const WARN: Self = Self(unsafe { NonZeroU8::new_unchecked(128) });
/// Entry represents a data validation error.
pub const ERROR: Self = Self(unsafe { NonZeroU8::new_unchecked(255) });
/// Gets the level name, if it is one of the `const` levels.
pub fn name(self) -> &'static str {
if self == Self::INFO {
"INFO"
} else if self == Self::WARN {
"WARN"
} else if self == Self::ERROR {
"ERROR"
} else {
""
}
}
}
impl fmt::Debug for DataNoteLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.name();
if name.is_empty() {
f.debug_tuple("DataNoteLevel").field(&self.0).finish()
} else {
if f.alternate() {
write!(f, "DataNoteLevel::")?;
}
write!(f, "{name}")
}
}
}
/// Represents an annotation set in a data context.
///
/// See [`DATA`] for more details.
#[derive(Clone)]
pub struct DataNote {
source: WidgetId,
level: DataNoteLevel,
value: std::sync::Weak<dyn DataNoteValue>,
}
impl fmt::Debug for DataNote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DataNote")
.field("source", &self.source)
.field("level", &self.level)
.field("value", &self.value())
.finish()
}
}
impl fmt::Display for DataNote {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(value) = self.value() {
write!(f, "{value}")
} else {
Ok(())
}
}
}
impl PartialEq for DataNote {
fn eq(&self, other: &Self) -> bool {
self.value.ptr_eq(&other.value) && self.source == other.source && self.level == other.level
}
}
impl DataNote {
/// New note.
pub fn new(source: WidgetId, level: DataNoteLevel, value: impl DataNoteValue + 'static) -> (Self, DataNoteHandle) {
let handle = Arc::new(value);
let value = Arc::downgrade(&handle);
(Self { source, level, value }, DataNoteHandle(Some(handle)))
}
/// Widget that setted the annotation.
pub fn source(&self) -> WidgetId {
self.source
}
/// Annotation level.
pub fn level(&self) -> DataNoteLevel {
self.level
}
/// Annotation value.
///
/// Is `None` if the note was dropped since last cleanup.
pub fn value(&self) -> Option<Arc<dyn DataNoteValue>> {
self.value.upgrade()
}
/// If the note is still valid.
pub fn retain(&self) -> bool {
self.value.strong_count() > 0
}
}
/// Handle for a [`DataNote`] in a context.
#[must_use = "dropping the handle drops the data note"]
pub struct DataNoteHandle(Option<Arc<dyn DataNoteValue>>);
impl DataNoteHandle {
/// New dummy handle.
pub fn dummy() -> Self {
Self(None)
}
/// If this is a dummy handle.
pub fn is_dummy(&self) -> bool {
self.0.is_some()
}
}
/// Represents a [`DataNote`] value.
///
/// # Trait Alias
///
/// This trait is used like a type alias for traits and is
/// already implemented for all types it applies to.
pub trait DataNoteValue: fmt::Debug + fmt::Display + Send + Sync + Any {
/// /// Access to `dyn Any` methods.
fn as_any(&self) -> &dyn Any;
}
impl<T: fmt::Debug + fmt::Display + Send + Sync + Any + 'static> DataNoteValue for T {
fn as_any(&self) -> &dyn Any {
self
}
}
/// Represents the data notes set in a context.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct DataNotes {
notes: Vec<DataNote>,
}
impl ops::Deref for DataNotes {
type Target = [DataNote];
fn deref(&self) -> &Self::Target {
&self.notes
}
}
impl DataNotes {
/// Remove dropped notes.
pub fn cleanup(&mut self) -> bool {
let len = self.notes.len();
self.notes.retain(|n| n.retain());
len != self.notes.len()
}
/// Clone notes of the same `level`.
pub fn clone_level(&self, level: DataNoteLevel) -> Self {
let mut notes = vec![];
for note in &self.notes {
if note.level == level {
notes.push(note.clone())
}
}
Self { notes }
}
/// Write all notes of the level to a text.
///
/// Multiple notes are placed each in a line.
pub fn level_txt(&self, level: DataNoteLevel) -> Txt {
let mut txt = Txt::from_string(String::new());
let mut sep = "";
for note in &self.notes {
if note.level == level {
if let Some(value) = note.value() {
use std::fmt::Write;
let _ = write!(&mut txt, "{sep}{value}");
sep = "\n";
}
}
}
txt.end_mut();
txt
}
}
impl fmt::Display for DataNotes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut sep = "";
for note in &self.notes {
if let Some(value) = note.value() {
write!(f, "{sep}{value}")?;
sep = "\n";
}
}
Ok(())
}
}
#[derive(Default)]
struct DataNotesProbe {
notes: DataNotes,
changed: bool,
}
/// Creates a note that samples [`DataNotes`] in a context.
///
/// The `on_changed` closure is called every time a note is inserted or removed in context. The closure
/// can be called in any [`UiNodeOp`], it is always called after the `child` processed the operation. The
/// notes always change to empty on deinit.
pub fn with_data_notes(child: impl UiNode, mut on_changed: impl FnMut(&DataNotes) + Send + 'static) -> impl UiNode {
let mut notes = None;
match_node(child, move |c, op| {
let is_deinit = match &op {
UiNodeOp::Init => {
notes = Some(Arc::new(RwLock::new(DataNotesProbe::default())));
false
}
UiNodeOp::Deinit => true,
_ => false,
};
DATA_NOTES_CTX.with_context(&mut notes, || c.op(op));
if is_deinit {
let n = notes.take().unwrap();
let not_empty = !mem::take(&mut n.write().notes).is_empty();
if not_empty {
on_changed(&DataNotes::default());
}
} else {
let notes = notes.as_ref().unwrap();
let mut notes = notes.write();
let cleaned = notes.notes.cleanup();
if mem::take(&mut notes.changed) || cleaned {
let notes = task::parking_lot::lock_api::RwLockWriteGuard::downgrade(notes);
let notes = &notes.notes;
if !DATA_NOTES_CTX.is_default() {
let parent = DATA_NOTES_CTX.get();
let mut parent = parent.write();
for note in notes.iter() {
if parent.notes.iter().all(|n| n != note) {
parent.notes.notes.push(note.clone());
parent.changed = true;
}
}
}
on_changed(notes);
}
}
})
}

View File

@ -0,0 +1,10 @@
[package]
name = "zero-ui-wgt-fill"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-wgt-view = { path = "../zero-ui-wgt-view" }

398
zero-ui-wgt-fill/src/lib.rs Normal file
View File

@ -0,0 +1,398 @@
//! Properties that fill the widget inner bounds and nodes that fill the available space.
use zero_ui_wgt::prelude::gradient::{stops, GradientRadius, GradientStops, LinearGradientAxis};
use zero_ui_wgt::{hit_test_mode, nodes::interactive_node, prelude::*, HitTestMode};
use zero_ui_wgt_view::*;
pub mod nodes;
/// Custom background property. Allows using any other widget as a background.
///
/// Backgrounds are not interactive, but are hit-testable, they don't influence the layout being measured and
/// arranged with the widget size, and they are always clipped to the widget bounds.
///
/// See also [`background_fn`] for use in styles.
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// background = Text! {
/// txt = "CUSTOM BACKGROUND";
/// font_size = 72;
/// font_color = web_colors::LIGHT_GRAY;
/// transform = Transform::new_rotate(45.deg());
/// align = Align::CENTER;
/// }
/// }
/// # ;
/// ```
///
/// The example renders a custom text background.
///
/// [`background_fn`]: fn@background_fn
#[property(FILL)]
pub fn background(child: impl UiNode, background: impl UiNode) -> impl UiNode {
let background = interactive_node(background, false);
let background = fill_node(background);
match_node_list(ui_vec![background, child], |children, op| match op {
UiNodeOp::Measure { wm, desired_size } => {
*desired_size = children.with_node(1, |n| n.measure(wm));
}
UiNodeOp::Layout { wl, final_size } => {
let size = children.with_node(1, |n| n.layout(wl));
LAYOUT.with_constraints(PxConstraints2d::new_exact_size(size), || {
children.with_node(0, |n| n.layout(wl));
});
*final_size = size;
}
_ => {}
})
}
/// Custom background generated using a [`WidgetFn<()>`].
///
/// This is the equivalent of setting [`background`] to the [`presenter`] node, but if the property is cloned
/// in styles the `wgt_fn` will be called multiple times to create duplicates of the background nodes instead
/// of moving the node to the latest widget.
///
/// [`WidgetFn<()>`]: WidgetFn
/// [`background`]: fn@background
#[property(FILL, default(WidgetFn::nil()))]
pub fn background_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
background(child, presenter((), wgt_fn))
}
/// Single color background property.
///
/// This property applies a [`flood`] as [`background`].
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// background_color = hex!(#ADF0B0);
/// }
/// # ;
/// ```
///
/// [`background`]: fn@background
#[property(FILL, default(colors::BLACK.transparent()))]
pub fn background_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
background(child, nodes::flood(color))
}
/// Linear gradient background property.
///
/// This property applies a [`linear_gradient`] as [`background`].
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// background_gradient = {
/// axis: 90.deg(),
/// stops: [colors::BLACK, colors::WHITE],
/// }
/// }
/// # ;
/// ```
///
/// [`background`]: fn@background
#[property(FILL, default(0.deg(), {
let c = colors::BLACK.transparent();
stops![c, c]
}))]
pub fn background_gradient(child: impl UiNode, axis: impl IntoVar<LinearGradientAxis>, stops: impl IntoVar<GradientStops>) -> impl UiNode {
background(child, nodes::linear_gradient(axis, stops))
}
/// Radial gradient background property.
///
/// This property applies a [`radial_gradient`] as [`background`].
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// background_radial = {
/// center: (50.pct(), 80.pct()),
/// radius: 100.pct(),
/// stops: [colors::BLACK, web_colors::DARK_ORANGE],
/// }
/// }
/// # ;
/// ```
///
/// [`background`]: fn@background
#[property(FILL, default((50.pct(), 50.pct()), 100.pct(), {
let c = colors::BLACK.transparent();
stops![c, c]
}))]
pub fn background_radial(
child: impl UiNode,
center: impl IntoVar<Point>,
radius: impl IntoVar<GradientRadius>,
stops: impl IntoVar<GradientStops>,
) -> impl UiNode {
background(child, nodes::radial_gradient(center, radius, stops))
}
/// Conic gradient background property.
///
/// This property applies a [`conic_gradient`] as [`background`].
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// background_conic = {
/// center: (50.pct(), 80.pct()),
/// angle: 0.deg(),
/// stops: [colors::BLACK, web_colors::DARK_ORANGE],
/// }
/// }
/// # ;
/// ```
///
/// [`background`]: fn@background
#[property(FILL, default((50.pct(), 50.pct()), 0.deg(), {
let c = colors::BLACK.transparent();
stops![c, c]
}))]
pub fn background_conic(
child: impl UiNode,
center: impl IntoVar<Point>,
angle: impl IntoVar<AngleRadian>,
stops: impl IntoVar<GradientStops>,
) -> impl UiNode {
background(child, nodes::conic_gradient(center, angle, stops))
}
/// Custom foreground fill property. Allows using any other widget as a foreground overlay.
///
/// The foreground is rendered over the widget content and background and under the widget borders.
///
/// Foregrounds are not interactive, not hit-testable and don't influence the widget layout.
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// foreground = Text! {
/// txt = "TRIAL";
/// font_size = 72;
/// font_color = colors::BLACK;
/// opacity = 10.pct();
/// transform = Transform::new_rotate(45.deg());
/// align = Align::CENTER;
/// }
/// }
/// # ;
/// ```
///
/// The example renders a custom see-through text overlay.
#[property(FILL, default(NilUiNode))]
pub fn foreground(child: impl UiNode, foreground: impl UiNode) -> impl UiNode {
let foreground = interactive_node(foreground, false);
let foreground = fill_node(foreground);
let foreground = hit_test_mode(foreground, HitTestMode::Disabled);
match_node_list(ui_vec![child, foreground], |children, op| match op {
UiNodeOp::Measure { wm, desired_size } => {
*desired_size = children.with_node(0, |n| n.measure(wm));
}
UiNodeOp::Layout { wl, final_size } => {
let size = children.with_node(0, |n| n.layout(wl));
LAYOUT.with_constraints(PxConstraints2d::new_exact_size(size), || {
children.with_node(1, |n| n.layout(wl));
});
*final_size = size;
}
_ => {}
})
}
/// Custom foreground generated using a [`WidgetFn<()>`].
///
/// This is the equivalent of setting [`foreground`] to the [`presenter`] node.
///
/// [`WidgetFn<()>`]: WidgetFn
/// [`foreground`]: fn@foreground
#[property(FILL, default(WidgetFn::nil()))]
pub fn foreground_fn(child: impl UiNode, wgt_fn: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
foreground(child, presenter((), wgt_fn))
}
/// Foreground highlight border overlay.
///
/// This property draws a border contour with extra `offsets` padding as an overlay.
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// Container! {
/// child = foo();
/// foreground_highlight = {
/// offsets: 3,
/// widths: 1,
/// sides: colors::BLUE,
/// }
/// }
/// # ;
/// ```
///
/// The example renders a solid blue 1 pixel border overlay, the border lines are offset 3 pixels into the container.
#[property(FILL, default(0, 0, BorderStyle::Hidden))]
pub fn foreground_highlight(
child: impl UiNode,
offsets: impl IntoVar<SideOffsets>,
widths: impl IntoVar<SideOffsets>,
sides: impl IntoVar<BorderSides>,
) -> impl UiNode {
let offsets = offsets.into_var();
let widths = widths.into_var();
let sides = sides.into_var();
let mut render_bounds = PxRect::zero();
let mut render_widths = PxSideOffsets::zero();
let mut render_radius = PxCornerRadius::zero();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&offsets).sub_var_layout(&widths).sub_var_render(&sides);
}
UiNodeOp::Layout { wl, final_size } => {
let size = child.layout(wl);
let radius = BORDER.inner_radius();
let offsets = offsets.layout();
let radius = radius.deflate(offsets);
let mut bounds = PxRect::zero();
if let Some(inline) = wl.inline() {
if let Some(first) = inline.rows.iter().find(|r| !r.size.is_empty()) {
bounds = *first;
}
}
if bounds.size.is_empty() {
let border_offsets = BORDER.inner_offsets();
bounds = PxRect::new(
PxPoint::new(offsets.left + border_offsets.left, offsets.top + border_offsets.top),
size - PxSize::new(offsets.horizontal(), offsets.vertical()),
);
}
let widths = LAYOUT.with_constraints(PxConstraints2d::new_exact_size(size), || widths.layout());
if render_bounds != bounds || render_widths != widths || render_radius != radius {
render_bounds = bounds;
render_widths = widths;
render_radius = radius;
WIDGET.render();
}
*final_size = size;
}
UiNodeOp::Render { frame } => {
child.render(frame);
frame.push_border(render_bounds, render_widths, sides.get(), render_radius);
}
_ => {}
})
}
/// Fill color overlay property.
///
/// This property applies a [`flood`] as [`foreground`].
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// foreground_color = rgba(0, 240, 0, 10.pct())
/// }
/// # ;
/// ```
///
/// The example adds a green tint to the container content.
///
/// [`foreground`]: fn@foreground
#[property(FILL, default(colors::BLACK.transparent()))]
pub fn foreground_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
foreground(child, nodes::flood(color))
}
/// Linear gradient overlay property.
///
/// This property applies a [`linear_gradient`] as [`foreground`] using the [`Clamp`] extend mode.
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _scope = App::minimal();
/// # fn foo() -> impl UiNode { Wgt!() }
/// #
/// Container! {
/// child = foo();
/// foreground_gradient = {
/// axis: (0, 0).to(0, 10),
/// stops: [colors::BLACK, colors::BLACK.transparent()]
/// }
/// }
/// # ;
/// ```
///
/// The example adds a *shadow* gradient to a 10px strip in the top part of the container content.
///
/// [`foreground`]: fn@foreground
/// [`Clamp`]: nodes::ExtendMode::Clamp
#[property(FILL, default(0.deg(), {
let c = colors::BLACK.transparent();
stops![c, c]
}))]
pub fn foreground_gradient(child: impl UiNode, axis: impl IntoVar<LinearGradientAxis>, stops: impl IntoVar<GradientStops>) -> impl UiNode {
foreground(child, nodes::linear_gradient(axis, stops))
}

View File

@ -0,0 +1,915 @@
//! Color and gradient fill nodes and builders.
use zero_ui_wgt::prelude::{gradient::*, *};
/// Gradient builder start.
///
/// Use [`gradient`] to start building.
///
/// [`gradient`]: fn@gradient
pub struct GradientBuilder<S> {
stops: S,
}
/// Starts building a gradient with the color stops.
pub fn gradient<S>(stops: S) -> GradientBuilder<S::Var>
where
S: IntoVar<GradientStops>,
{
GradientBuilder { stops: stops.into_var() }
}
/// Starts building a linear gradient with the axis and color stops.
///
/// Returns a node that is also a builder that can be used to refine the gradient definition.
pub fn linear_gradient<A: IntoVar<LinearGradientAxis>, S: IntoVar<GradientStops>>(
axis: A,
stops: S,
) -> LinearGradient<S::Var, A::Var, LocalVar<ExtendMode>> {
gradient(stops).linear(axis)
}
/// Starts building a radial gradient with the radius and color stops.
///
/// Returns a node that is also a builder that can be used to refine the gradient definition.
pub fn radial_gradient<C, R, S>(center: C, radius: R, stops: S) -> RadialGradient<S::Var, C::Var, R::Var, LocalVar<ExtendMode>>
where
C: IntoVar<Point>,
R: IntoVar<GradientRadius>,
S: IntoVar<GradientStops>,
{
gradient(stops).radial(center, radius)
}
/// Starts building a conic gradient with the angle and color stops.
///
/// Returns a node that is also a builder that can be used to refine the gradient definition.
pub fn conic_gradient<C, A, S>(center: C, angle: A, stops: S) -> ConicGradient<S::Var, C::Var, A::Var, LocalVar<ExtendMode>>
where
C: IntoVar<Point>,
A: IntoVar<AngleRadian>,
S: IntoVar<GradientStops>,
{
gradient(stops).conic(center, angle)
}
impl<S> GradientBuilder<S>
where
S: Var<GradientStops>,
{
/// Builds a linear gradient.
///
/// Returns a node that fills the available space with the gradient, the node type doubles
/// as a builder that can continue building a linear gradient.
pub fn linear<A>(self, axis: A) -> LinearGradient<S, A::Var, LocalVar<ExtendMode>>
where
A: IntoVar<LinearGradientAxis>,
{
LinearGradient {
stops: self.stops,
axis: axis.into_var(),
extend_mode: ExtendMode::Clamp.into_var(),
data: LinearNodeData::default(),
}
}
/// Builds a radial gradient.
///
/// Returns a node that fills the available space with the gradient, the node type doubles
/// as a builder that can continue building a radial gradient.
pub fn radial<C, R>(self, center: C, radius: R) -> RadialGradient<S, C::Var, R::Var, LocalVar<ExtendMode>>
where
C: IntoVar<Point>,
R: IntoVar<GradientRadius>,
{
RadialGradient {
stops: self.stops,
center: center.into_var(),
radius: radius.into_var(),
extend_mode: ExtendMode::Clamp.into_var(),
data: RadialNodeData::default(),
}
}
/// Builds a conic gradient.
///
/// Returns a node that fills the available space with the gradient, the node type doubles
/// as a builder that can continue building a conic gradient.
pub fn conic<C: IntoVar<Point>, A: IntoVar<AngleRadian>>(
self,
center: C,
angle: A,
) -> ConicGradient<S, C::Var, A::Var, LocalVar<ExtendMode>> {
ConicGradient {
stops: self.stops,
center: center.into_var(),
angle: angle.into_var(),
extend_mode: ExtendMode::Clamp.into_var(),
data: ConicNodeData::default(),
}
}
}
/// Linear gradient.
///
/// Can be used as a node that fills the available space with the gradient, or can continue building a linear
/// gradient.
///
/// Use [`gradient`] or [`linear_gradient`] to build.
///
/// [`gradient`]: fn@gradient
pub struct LinearGradient<S, A, E> {
stops: S,
axis: A,
extend_mode: E,
data: LinearNodeData,
}
impl<S, A, E> LinearGradient<S, A, E>
where
S: Var<GradientStops>,
A: Var<LinearGradientAxis>,
E: Var<ExtendMode>,
{
/// Sets the extend mode of the linear gradient.
///
/// By default is [`ExtendMode::Clamp`].
pub fn extend_mode<E2: IntoVar<ExtendMode>>(self, mode: E2) -> LinearGradient<S, A, E2::Var> {
LinearGradient {
stops: self.stops,
axis: self.axis,
extend_mode: mode.into_var(),
data: self.data,
}
}
/// Sets the extend mode to [`ExtendMode::Repeat`].
pub fn repeat(self) -> LinearGradient<S, A, LocalVar<ExtendMode>> {
self.extend_mode(ExtendMode::Repeat)
}
/// Sets the extend mode to [`ExtendMode::Reflect`].
pub fn reflect(self) -> LinearGradient<S, A, LocalVar<ExtendMode>> {
self.extend_mode(ExtendMode::Reflect)
}
/// Continue building a tiled linear gradient.
pub fn tile<T, TS>(self, tile_size: T, tile_spacing: TS) -> TiledLinearGradient<S, A, E, T::Var, TS::Var>
where
T: IntoVar<Size>,
TS: IntoVar<Size>,
{
TiledLinearGradient {
stops: self.stops,
axis: self.axis,
extend_mode: self.extend_mode,
tile_size: tile_size.into_var(),
tile_spacing: tile_spacing.into_var(),
data: self.data,
tile_data: TiledNodeData::default(),
}
}
/// Continue building a tiled linear gradient.
///
/// Relative values are resolved on the full available size, so settings this to `100.pct()` is
/// the same as not tiling.
pub fn tile_size<T>(self, size: T) -> TiledLinearGradient<S, A, E, T::Var, LocalVar<Size>>
where
T: IntoVar<Size>,
{
self.tile(size, Size::zero())
}
}
/// Tiled linear gradient.
///
/// Can be used as a node that fills the available space with the gradient tiles, or can continue building a
/// repeating linear gradient.
///
///
/// Use [`gradient`], [`linear_gradient`] to build.
///
/// [`gradient`]: fn@gradient
pub struct TiledLinearGradient<S, A, E, T, TS> {
stops: S,
axis: A,
extend_mode: E,
tile_size: T,
tile_spacing: TS,
data: LinearNodeData,
tile_data: TiledNodeData,
}
impl<S, A, E, T, TS> TiledLinearGradient<S, A, E, T, TS>
where
S: Var<GradientStops>,
A: Var<LinearGradientAxis>,
E: Var<ExtendMode>,
T: Var<Size>,
TS: Var<Size>,
{
/// Set the space between tiles.
///
/// Relative values are resolved on the tile size, so setting this to `100.pct()` will
/// *skip* a tile.
///
/// Leftover values are resolved on the space taken by tiles that do not
/// fully fit in the available space, so setting this to `1.lft()` will cause the *border* tiles
/// to always touch the full bounds and the middle filled with the maximum full tiles that fit or
/// empty space.
pub fn tile_spacing<TS2>(self, spacing: TS2) -> TiledLinearGradient<S, A, E, T::Var, TS2::Var>
where
TS2: IntoVar<Size>,
{
TiledLinearGradient {
stops: self.stops,
axis: self.axis,
extend_mode: self.extend_mode,
tile_size: self.tile_size,
tile_spacing: spacing.into_var(),
data: self.data,
tile_data: self.tile_data,
}
}
}
/// Radial gradient.
///
/// Can be used as a node that fills the available space with the gradient, or can continue building a linear
/// gradient.
///
/// Use [`gradient`] or [`radial_gradient`] to build.
///
/// [`gradient`]: fn@gradient
pub struct RadialGradient<S, C, R, E> {
stops: S,
center: C,
radius: R,
extend_mode: E,
data: RadialNodeData,
}
impl<S, C, R, E> RadialGradient<S, C, R, E>
where
S: Var<GradientStops>,
C: Var<Point>,
R: Var<GradientRadius>,
E: Var<ExtendMode>,
{
/// Sets the extend mode of the radial gradient.
///
/// By default is [`ExtendMode::Clamp`].
pub fn extend_mode<E2: IntoVar<ExtendMode>>(self, mode: E2) -> RadialGradient<S, C, R, E2::Var> {
RadialGradient {
stops: self.stops,
center: self.center,
radius: self.radius,
extend_mode: mode.into_var(),
data: self.data,
}
}
/// Sets the extend mode to [`ExtendMode::Repeat`].
pub fn repeat(self) -> RadialGradient<S, C, R, LocalVar<ExtendMode>> {
self.extend_mode(ExtendMode::Repeat)
}
/// Sets the extend mode to [`ExtendMode::Reflect`].
pub fn reflect(self) -> RadialGradient<S, C, R, LocalVar<ExtendMode>> {
self.extend_mode(ExtendMode::Reflect)
}
/// Continue building a tiled radial gradient.
pub fn tile<T, TS>(self, tile_size: T, tile_spacing: TS) -> TiledRadialGradient<S, C, R, E, T::Var, TS::Var>
where
T: IntoVar<Size>,
TS: IntoVar<Size>,
{
TiledRadialGradient {
stops: self.stops,
center: self.center,
radius: self.radius,
extend_mode: self.extend_mode,
tile_size: tile_size.into_var(),
tile_spacing: tile_spacing.into_var(),
data: self.data,
tile_data: TiledNodeData::default(),
}
}
/// Continue building a tiled radial gradient.
pub fn tile_size<T>(self, size: T) -> TiledRadialGradient<S, C, R, E, T::Var, LocalVar<Size>>
where
T: IntoVar<Size>,
{
self.tile(size, Size::zero())
}
}
/// Tiled radial gradient.
///
/// Can be used as a node that fills the available space with the gradient tiles, or can continue building the gradient.
///
///
/// Use [`gradient`], [`radial_gradient`] to build.
///
/// [`gradient`]: fn@gradient
pub struct TiledRadialGradient<S, C, R, E, T, TS> {
stops: S,
center: C,
radius: R,
extend_mode: E,
tile_size: T,
tile_spacing: TS,
data: RadialNodeData,
tile_data: TiledNodeData,
}
impl<S, C, R, E, T, TS> TiledRadialGradient<S, C, R, E, T, TS>
where
S: Var<GradientStops>,
C: Var<Point>,
R: Var<GradientRadius>,
E: Var<ExtendMode>,
T: Var<Size>,
TS: Var<Size>,
{
/// Set the space between tiles.
///
/// Relative values are resolved on the tile size, so setting this to `100.pct()` will
/// *skip* a tile.
///
/// Leftover values are resolved on the space taken by tiles that do not
/// fully fit in the available space, so setting this to `1.lft()` will cause the *border* tiles
/// to always touch the full bounds and the middle filled with the maximum full tiles that fit or
/// empty space.
pub fn tile_spacing<TS2>(self, spacing: TS2) -> TiledRadialGradient<S, C, R, E, T::Var, TS2::Var>
where
TS2: IntoVar<Size>,
{
TiledRadialGradient {
stops: self.stops,
center: self.center,
radius: self.radius,
extend_mode: self.extend_mode,
tile_size: self.tile_size,
tile_spacing: spacing.into_var(),
data: self.data,
tile_data: self.tile_data,
}
}
}
/// Conic gradient.
///
/// Can be used as a node that fills the available space with the gradient, or can continue building a linear
/// gradient.
///
/// Use [`gradient`] or [`conic_gradient`] to build.
///
/// [`gradient`]: fn@gradient
pub struct ConicGradient<S, C, A, E> {
stops: S,
center: C,
angle: A,
extend_mode: E,
data: ConicNodeData,
}
impl<S, C, A, E> ConicGradient<S, C, A, E>
where
S: Var<GradientStops>,
C: Var<Point>,
A: Var<AngleRadian>,
E: Var<ExtendMode>,
{
/// Sets the extend mode of the conic gradient.
///
/// By default is [`ExtendMode::Clamp`].
pub fn extend_mode<E2: IntoVar<ExtendMode>>(self, mode: E2) -> ConicGradient<S, C, A, E2::Var> {
ConicGradient {
stops: self.stops,
center: self.center,
angle: self.angle,
extend_mode: mode.into_var(),
data: self.data,
}
}
/// Sets the extend mode to [`ExtendMode::Repeat`].
pub fn repeat(self) -> ConicGradient<S, C, A, LocalVar<ExtendMode>> {
self.extend_mode(ExtendMode::Repeat)
}
/// Sets the extend mode to [`ExtendMode::Reflect`].
pub fn reflect(self) -> ConicGradient<S, C, A, LocalVar<ExtendMode>> {
self.extend_mode(ExtendMode::Reflect)
}
/// Continue building a tiled radial gradient.
pub fn tile<T, TS>(self, tile_size: T, tile_spacing: TS) -> TiledConicGradient<S, C, A, E, T::Var, TS::Var>
where
T: IntoVar<Size>,
TS: IntoVar<Size>,
{
TiledConicGradient {
stops: self.stops,
center: self.center,
angle: self.angle,
extend_mode: self.extend_mode,
tile_size: tile_size.into_var(),
tile_spacing: tile_spacing.into_var(),
data: self.data,
tile_data: TiledNodeData::default(),
}
}
/// Continue building a tiled radial gradient.
pub fn tile_size<T>(self, size: T) -> TiledConicGradient<S, C, A, E, T::Var, LocalVar<Size>>
where
T: IntoVar<Size>,
{
self.tile(size, Size::zero())
}
}
/// Tiled conic gradient.
///
/// Can be used as a node that fills the available space with the gradient tiles, or can continue building the gradient.
///
/// Use [`gradient`], [`conic_gradient`] to build.
///
/// [`gradient`]: fn@gradient
pub struct TiledConicGradient<S, C, A, E, T, TS> {
stops: S,
center: C,
angle: A,
extend_mode: E,
tile_size: T,
tile_spacing: TS,
data: ConicNodeData,
tile_data: TiledNodeData,
}
impl<S, C, A, E, T, TS> TiledConicGradient<S, C, A, E, T, TS>
where
S: Var<GradientStops>,
C: Var<Point>,
A: Var<AngleRadian>,
E: Var<ExtendMode>,
T: Var<Size>,
TS: Var<Size>,
{
/// Set the space between tiles.
///
/// Relative values are resolved on the tile size, so setting this to `100.pct()` will
/// *skip* a tile.
///
/// Leftover values are resolved on the space taken by tiles that do not
/// fully fit in the available space, so setting this to `1.lft()` will cause the *border* tiles
/// to always touch the full bounds and the middle filled with the maximum full tiles that fit or
/// empty space.
pub fn tile_spacing<TS2>(self, spacing: TS2) -> TiledConicGradient<S, C, A, E, T::Var, TS2::Var>
where
TS2: IntoVar<Size>,
{
TiledConicGradient {
stops: self.stops,
center: self.center,
angle: self.angle,
extend_mode: self.extend_mode,
tile_size: self.tile_size,
tile_spacing: spacing.into_var(),
data: self.data,
tile_data: self.tile_data,
}
}
}
#[derive(Default)]
struct LinearNodeData {
line: PxLine,
stops: Vec<RenderGradientStop>,
size: PxSize,
}
#[ui_node(none)]
impl<S, A, E> UiNode for LinearGradient<S, A, E>
where
S: Var<GradientStops>,
A: Var<LinearGradientAxis>,
E: Var<ExtendMode>,
{
fn init(&mut self) {
WIDGET
.sub_var_layout(&self.axis)
.sub_var_layout(&self.stops)
.sub_var_layout(&self.extend_mode);
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
LAYOUT.constraints().fill_size()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
let size = LAYOUT.constraints().fill_size();
if self.data.size != size {
self.data.size = size;
self.data.line = self.axis.layout();
let length = self.data.line.length();
LAYOUT.with_constraints(LAYOUT.constraints().with_new_exact_x(length), || {
self.stops
.with(|s| s.layout_linear(LayoutAxis::X, self.extend_mode.get(), &mut self.data.line, &mut self.data.stops))
});
WIDGET.render();
}
size
}
fn render(&mut self, frame: &mut FrameBuilder) {
frame.push_linear_gradient(
PxRect::from_size(self.data.size),
self.data.line,
&self.data.stops,
self.extend_mode.get().into(),
self.data.size,
PxSize::zero(),
);
}
}
#[derive(Default)]
struct TiledNodeData {
size: PxSize,
spacing: PxSize,
}
#[ui_node(none)]
impl<S, A, E, T, TS> UiNode for TiledLinearGradient<S, A, E, T, TS>
where
S: Var<GradientStops>,
A: Var<LinearGradientAxis>,
E: Var<ExtendMode>,
T: Var<Size>,
TS: Var<Size>,
{
fn init(&mut self) {
WIDGET
.sub_var_layout(&self.axis)
.sub_var_layout(&self.stops)
.sub_var_layout(&self.extend_mode)
.sub_var_layout(&self.tile_size)
.sub_var_layout(&self.tile_spacing);
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
LAYOUT.constraints().fill_size()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
let constraints = LAYOUT.constraints();
let size = constraints.fill_size();
if self.data.size != size {
self.data.size = size;
self.tile_data.size = self.tile_size.layout_dft(self.data.size);
LAYOUT.with_constraints(PxConstraints2d::new_exact_size(self.tile_data.size), || {
let leftover = tile_leftover(self.tile_data.size, size);
LAYOUT.with_leftover(Some(leftover.width), Some(leftover.height), || {
self.tile_data.spacing = self.tile_spacing.layout();
});
self.data.line = self.axis.layout();
});
let length = self.data.line.length();
LAYOUT.with_constraints(constraints.with_new_exact_x(length), || {
self.stops
.with(|s| s.layout_linear(LayoutAxis::X, self.extend_mode.get(), &mut self.data.line, &mut self.data.stops))
});
WIDGET.render();
}
size
}
fn render(&mut self, frame: &mut FrameBuilder) {
frame.push_linear_gradient(
PxRect::from_size(self.data.size),
self.data.line,
&self.data.stops,
self.extend_mode.get().into(),
self.tile_data.size,
self.tile_data.spacing,
);
}
}
#[derive(Default)]
struct RadialNodeData {
size: PxSize,
center: PxPoint,
radius: PxSize,
stops: Vec<RenderGradientStop>,
}
#[ui_node(none)]
impl<S, C, R, E> UiNode for RadialGradient<S, C, R, E>
where
S: Var<GradientStops>,
C: Var<Point>,
R: Var<GradientRadius>,
E: Var<ExtendMode>,
{
fn init(&mut self) {
WIDGET
.sub_var_layout(&self.center)
.sub_var_layout(&self.radius)
.sub_var_layout(&self.stops)
.sub_var_layout(&self.extend_mode);
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
LAYOUT.constraints().fill_size()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
let size = LAYOUT.constraints().fill_size();
if size != self.data.size {
self.data.size = size;
LAYOUT.with_constraints(PxConstraints2d::new_fill_size(size), || {
self.data.center = self.center.layout_dft(size.to_vector().to_point() * 0.5.fct());
self.data.radius = self.radius.get().layout(self.data.center);
});
LAYOUT.with_constraints(
LAYOUT
.constraints()
.with_exact_x(self.data.radius.width.max(self.data.radius.height)),
|| {
self.stops
.with(|s| s.layout_radial(LayoutAxis::X, self.extend_mode.get(), &mut self.data.stops))
},
);
WIDGET.render();
}
size
}
fn render(&mut self, frame: &mut FrameBuilder) {
frame.push_radial_gradient(
PxRect::from_size(self.data.size),
self.data.center,
self.data.radius,
&self.data.stops,
self.extend_mode.get().into(),
self.data.size,
PxSize::zero(),
);
}
}
#[ui_node(none)]
impl<S, C, R, E, T, TS> UiNode for TiledRadialGradient<S, C, R, E, T, TS>
where
S: Var<GradientStops>,
C: Var<Point>,
R: Var<GradientRadius>,
E: Var<ExtendMode>,
T: Var<Size>,
TS: Var<Size>,
{
fn init(&mut self) {
WIDGET
.sub_var_layout(&self.center)
.sub_var_layout(&self.radius)
.sub_var_layout(&self.stops)
.sub_var_layout(&self.extend_mode)
.sub_var_layout(&self.tile_size)
.sub_var_layout(&self.tile_spacing);
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
LAYOUT.constraints().fill_size()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
let size = LAYOUT.constraints().fill_size();
if size != self.data.size {
self.data.size = size;
self.tile_data.size = self.tile_size.layout_dft(size);
LAYOUT.with_constraints(PxConstraints2d::new_exact_size(self.tile_data.size), || {
let leftover = tile_leftover(self.tile_data.size, size);
LAYOUT.with_leftover(Some(leftover.width), Some(leftover.height), || {
self.tile_data.spacing = self.tile_spacing.layout();
});
self.data.center = self.center.layout_dft(self.tile_data.size.to_vector().to_point() * 0.5.fct());
self.data.radius = self.radius.get().layout(self.data.center);
});
LAYOUT.with_constraints(
LAYOUT
.constraints()
.with_exact_x(self.data.radius.width.max(self.data.radius.height)),
|| {
self.stops
.with(|s| s.layout_radial(LayoutAxis::X, self.extend_mode.get(), &mut self.data.stops))
},
);
WIDGET.render();
}
size
}
fn render(&mut self, frame: &mut FrameBuilder) {
frame.push_radial_gradient(
PxRect::from_size(self.data.size),
self.data.center,
self.data.radius,
&self.data.stops,
self.extend_mode.get().into(),
self.tile_data.size,
self.tile_data.spacing,
);
}
}
#[derive(Default)]
struct ConicNodeData {
size: PxSize,
center: PxPoint,
stops: Vec<RenderGradientStop>,
}
#[ui_node(none)]
impl<S, C, A, E> UiNode for ConicGradient<S, C, A, E>
where
S: Var<GradientStops>,
C: Var<Point>,
A: Var<AngleRadian>,
E: Var<ExtendMode>,
{
fn init(&mut self) {
WIDGET
.sub_var_layout(&self.center)
.sub_var_layout(&self.angle)
.sub_var_layout(&self.stops)
.sub_var_layout(&self.extend_mode);
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
LAYOUT.constraints().fill_size()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
let size = LAYOUT.constraints().fill_size();
if size != self.data.size {
self.data.size = size;
LAYOUT.with_constraints(PxConstraints2d::new_fill_size(size), || {
self.data.center = self.center.layout_dft(size.to_vector().to_point() * 0.5.fct());
});
let perimeter = Px({
let a = size.width.0 as f32;
let b = size.height.0 as f32;
std::f32::consts::PI * 2.0 * ((a * a + b * b) / 2.0).sqrt()
} as _);
LAYOUT.with_constraints(LAYOUT.constraints().with_exact_x(perimeter), || {
self.stops
.with(|s| s.layout_radial(LayoutAxis::X, self.extend_mode.get(), &mut self.data.stops))
});
WIDGET.render();
}
size
}
fn render(&mut self, frame: &mut FrameBuilder) {
frame.push_conic_gradient(
PxRect::from_size(self.data.size),
self.data.center,
self.angle.get(),
&self.data.stops,
self.extend_mode.get().into(),
self.data.size,
PxSize::zero(),
);
}
}
#[ui_node(none)]
impl<S, C, A, E, T, TS> UiNode for TiledConicGradient<S, C, A, E, T, TS>
where
S: Var<GradientStops>,
C: Var<Point>,
A: Var<AngleRadian>,
E: Var<ExtendMode>,
T: Var<Size>,
TS: Var<Size>,
{
fn init(&mut self) {
WIDGET
.sub_var_layout(&self.center)
.sub_var_layout(&self.angle)
.sub_var_layout(&self.stops)
.sub_var_layout(&self.extend_mode)
.sub_var_layout(&self.tile_size)
.sub_var_layout(&self.tile_spacing);
}
fn measure(&mut self, _: &mut WidgetMeasure) -> PxSize {
LAYOUT.constraints().fill_size()
}
fn layout(&mut self, _: &mut WidgetLayout) -> PxSize {
let size = LAYOUT.constraints().fill_size();
if size != self.data.size {
self.data.size = size;
self.tile_data.size = self.tile_size.layout_dft(size);
LAYOUT.with_constraints(PxConstraints2d::new_exact_size(self.tile_data.size), || {
let leftover = tile_leftover(self.tile_data.size, size);
LAYOUT.with_leftover(Some(leftover.width), Some(leftover.height), || {
self.tile_data.spacing = self.tile_spacing.layout();
});
self.data.center = self.center.get().layout_dft(self.tile_data.size.to_vector().to_point() * 0.5.fct());
});
let perimeter = Px({
let a = self.tile_data.size.width.0 as f32;
let b = self.tile_data.size.height.0 as f32;
std::f32::consts::PI * 2.0 * ((a * a + b * b) / 2.0).sqrt()
} as _);
LAYOUT.with_constraints(LAYOUT.constraints().with_exact_x(perimeter), || {
self.stops
.with(|s| s.layout_radial(LayoutAxis::X, self.extend_mode.get(), &mut self.data.stops))
});
WIDGET.render();
}
size
}
fn render(&mut self, frame: &mut FrameBuilder) {
frame.push_conic_gradient(
PxRect::from_size(self.data.size),
self.data.center,
self.angle.get(),
&self.data.stops,
self.extend_mode.get().into(),
self.tile_data.size,
self.tile_data.spacing,
);
}
}
/// Node that fills the widget area with a color.
///
/// Note that this node is not a full widget, it can be used as part of an widget without adding to the info tree.
pub fn flood(color: impl IntoVar<Rgba>) -> impl UiNode {
let color = color.into_var();
let mut render_size = PxSize::zero();
let frame_key = FrameValueKey::new_unique();
match_node_leaf(move |op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render_update(&color);
}
UiNodeOp::Measure { desired_size, .. } => {
*desired_size = LAYOUT.constraints().fill_size();
}
UiNodeOp::Layout { final_size, .. } => {
*final_size = LAYOUT.constraints().fill_size();
if *final_size != render_size {
render_size = *final_size;
WIDGET.render();
}
}
UiNodeOp::Render { frame } => {
frame.push_color(PxRect::from_size(render_size), frame_key.bind_var(&color, |&c| c.into()));
}
UiNodeOp::RenderUpdate { update } => {
update.update_color_opt(frame_key.update_var(&color, |&c| c.into()));
}
_ => {}
})
}
fn tile_leftover(tile_size: PxSize, wgt_size: PxSize) -> PxSize {
if tile_size.is_empty() || wgt_size.is_empty() {
return PxSize::zero();
}
let full_leftover_x = wgt_size.width % tile_size.width;
let full_leftover_y = wgt_size.height % tile_size.height;
let full_tiles_x = wgt_size.width / tile_size.width;
let full_tiles_y = wgt_size.height / tile_size.height;
let spaces_x = full_tiles_x - Px(1);
let spaces_y = full_tiles_y - Px(1);
PxSize::new(
if spaces_x > Px(0) { full_leftover_x / spaces_x } else { Px(0) },
if spaces_y > Px(0) { full_leftover_y / spaces_y } else { Px(0) },
)
}

View File

@ -0,0 +1,9 @@
[package]
name = "zero-ui-wgt-filters"
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-wgt = { path = "../zero-ui-wgt" }

View File

@ -0,0 +1,534 @@
//! Color filter properties, [`opacity`](fn@opacity), [`filter`](fn@filter) and more.
use color_filters::{self as cf, Filter};
use zero_ui_wgt::prelude::*;
/// Color filter, or combination of filters.
///
/// This property allows setting multiple filters at once, there is also a property for every
/// filter for easier value updating.
///
/// # Performance
///
/// The performance for setting specific filter properties versus this one is the same, except for [`opacity`]
/// which can be animated using only frame updates instead of generating a new frame every change.
///
/// [`opacity`]: fn@opacity
#[property(CONTEXT, default(Filter::default()))]
pub fn filter(child: impl UiNode, filter: impl IntoVar<Filter>) -> impl UiNode {
filter_any(child, filter, false)
}
/// Backdrop filter, or combination of filters.
///
/// This property allows setting multiple filters at once, there is also a property for every
/// filter for easier value updating.
///
/// The filters are applied to everything rendered behind the widget.
///
/// # Performance
///
/// The performance for setting specific filter properties versus this one is the same.
///
/// [`opacity`]: fn@opacity
#[property(CONTEXT, default(Filter::default()))]
pub fn backdrop_filter(child: impl UiNode, filter: impl IntoVar<Filter>) -> impl UiNode {
backdrop_filter_any(child, filter)
}
/// Color filter, or combination of filters targeting the widget's descendants and not the widget itself.
///
/// This property allows setting multiple filters at once, there is also a property for every
/// filter for easier value updating.
///
/// # Performance
///
/// The performance for setting specific filter properties versus this one is the same, except for [`child_opacity`]
/// which can be animated using only frame updates instead of generating a new frame every change.
///
/// [`child_opacity`]: fn@child_opacity
#[property(CHILD_CONTEXT, default(Filter::default()))]
pub fn child_filter(child: impl UiNode, filter: impl IntoVar<Filter>) -> impl UiNode {
filter_any(child, filter, true)
}
/// Inverts the colors of the widget.
///
/// Zero does not invert, one fully inverts.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::invert`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(false))]
pub fn invert_color(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
filter_render(child, amount.into_var().map(|&a| cf::invert(a)), false)
}
/// Inverts the colors of everything behind the widget.
///
/// Zero does not invert, one fully inverts.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::invert`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(false))]
pub fn backdrop_invert(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
backdrop_filter_render(child, amount.into_var().map(|&a| cf::invert(a)))
}
/// Blur the widget.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::blur`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(0))]
pub fn blur(child: impl UiNode, radius: impl IntoVar<Length>) -> impl UiNode {
filter_layout(child, radius.into_var().map(|r| cf::blur(r.clone())), false)
}
/// Blur the everything behind the widget.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::blur`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(0))]
pub fn backdrop_blur(child: impl UiNode, radius: impl IntoVar<Length>) -> impl UiNode {
backdrop_filter_layout(child, radius.into_var().map(|r| cf::blur(r.clone())))
}
/// Sepia tone the widget.
///
/// zero is the original colors, one is the full desaturated brown look.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::sepia`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(false))]
pub fn sepia(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
filter_render(child, amount.into_var().map(|&a| cf::sepia(a)), false)
}
/// Sepia tone everything behind the widget.
///
/// zero is the original colors, one is the full desaturated brown look.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::sepia`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(false))]
pub fn backdrop_sepia(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
backdrop_filter_render(child, amount.into_var().map(|&a| cf::sepia(a)))
}
/// Grayscale tone the widget.
///
/// Zero is the original colors, one if the full grayscale.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::grayscale`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(false))]
pub fn grayscale(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
filter_render(child, amount.into_var().map(|&a| cf::grayscale(a)), false)
}
/// Grayscale tone everything behind the widget.
///
/// Zero is the original colors, one if the full grayscale.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::grayscale`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(false))]
pub fn backdrop_grayscale(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
backdrop_filter_render(child, amount.into_var().map(|&a| cf::grayscale(a)))
}
/// Drop-shadow effect for the widget.
///
/// The shadow is *pixel accurate*.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::drop_shadow`] using variable merging.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default((0, 0), 0, colors::BLACK.transparent()))]
pub fn drop_shadow(
child: impl UiNode,
offset: impl IntoVar<Point>,
blur_radius: impl IntoVar<Length>,
color: impl IntoVar<Rgba>,
) -> impl UiNode {
filter_layout(
child,
merge_var!(offset.into_var(), blur_radius.into_var(), color.into_var(), |o, r, &c| {
cf::drop_shadow(o.clone(), r.clone(), c)
}),
false,
)
}
/// Adjust the widget colors brightness.
///
/// Zero removes all brightness, one is the original brightness.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::brightness`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(1.0))]
pub fn brightness(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
filter_render(child, amount.into_var().map(|&a| cf::brightness(a)), false)
}
/// Adjust color brightness of everything behind the widget.
///
/// Zero removes all brightness, one is the original brightness.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::brightness`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(1.0))]
pub fn backdrop_brightness(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
backdrop_filter_render(child, amount.into_var().map(|&a| cf::brightness(a)))
}
/// Adjust the widget colors contrast.
///
/// Zero removes all contrast, one is the original contrast.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::brightness`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(1.0))]
pub fn contrast(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
filter_render(child, amount.into_var().map(|&a| cf::contrast(a)), false)
}
/// Adjust the color contrast of everything behind the widget.
///
/// Zero removes all contrast, one is the original contrast.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::brightness`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(1.0))]
pub fn backdrop_contrast(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
backdrop_filter_render(child, amount.into_var().map(|&a| cf::contrast(a)))
}
/// Adjust the widget colors saturation.
///
/// Zero fully desaturates, one is the original saturation.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::saturate`] using variable mapping.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(1.0))]
pub fn saturate(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
filter_render(child, amount.into_var().map(|&a| cf::saturate(a)), false)
}
/// Adjust color saturation of everything behind the widget.
///
/// Zero fully desaturates, one is the original saturation.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::saturate`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
#[property(CONTEXT, default(1.0))]
pub fn backdrop_saturate(child: impl UiNode, amount: impl IntoVar<Factor>) -> impl UiNode {
backdrop_filter_render(child, amount.into_var().map(|&a| cf::saturate(a)))
}
/// Hue shift the widget colors.
///
/// Adds `angle` to the [`hue`] of the widget colors.
///
/// This property is a shorthand way of setting [`filter`] to [`color::filters::hue_rotate`] using variable mapping.
///
/// [`filter`]: fn@filter
/// [`hue`]: Hsla::hue
#[property(CONTEXT, default(0.deg()))]
pub fn hue_rotate(child: impl UiNode, angle: impl IntoVar<AngleDegree>) -> impl UiNode {
filter_render(child, angle.into_var().map(|&a| cf::hue_rotate(a)), false)
}
/// Hue shift the colors behind the widget.
///
/// Adds `angle` to the [`hue`] of the widget colors.
///
/// This property is a shorthand way of setting [`backdrop_filter`] to [`color::filters::hue_rotate`] using variable mapping.
///
/// [`backdrop_filter`]: fn@backdrop_filter
/// [`hue`]: Hsla::hue
#[property(CONTEXT, default(0.deg()))]
pub fn backdrop_hue_rotate(child: impl UiNode, angle: impl IntoVar<AngleDegree>) -> impl UiNode {
backdrop_filter_render(child, angle.into_var().map(|&a| cf::hue_rotate(a)))
}
/// Custom color filter.
///
/// The color matrix is in the format of SVG color matrix, [0..5] is the first matrix row.
#[property(CONTEXT, default(cf::ColorMatrix::identity()))]
pub fn color_matrix(child: impl UiNode, matrix: impl IntoVar<cf::ColorMatrix>) -> impl UiNode {
filter_render(child, matrix.into_var().map(|&m| cf::color_matrix(m)), false)
}
/// Custom backdrop filter.
///
/// The color matrix is in the format of SVG color matrix, [0..5] is the first matrix row.
#[property(CONTEXT, default(cf::ColorMatrix::identity()))]
pub fn backdrop_color_matrix(child: impl UiNode, matrix: impl IntoVar<cf::ColorMatrix>) -> impl UiNode {
backdrop_filter_render(child, matrix.into_var().map(|&m| cf::color_matrix(m)))
}
/// Opacity/transparency of the widget.
///
/// This property provides the same visual result as setting [`filter`] to [`color::filters::opacity(opacity)`](color::filters::opacity),
/// **but** updating the opacity is faster in this property.
///
/// [`filter`]: fn@filter
#[property(CONTEXT, default(1.0))]
pub fn opacity(child: impl UiNode, alpha: impl IntoVar<Factor>) -> impl UiNode {
opacity_impl(child, alpha, false)
}
/// Opacity/transparency of the widget's child.
///
/// This property provides the same visual result as setting [`child_filter`] to [`color::filters::opacity(opacity)`](color::filters::opacity),
/// **but** updating the opacity is faster in this property.
///
/// [`child_filter`]: fn@child_filter
#[property(CHILD_CONTEXT, default(1.0))]
pub fn child_opacity(child: impl UiNode, alpha: impl IntoVar<Factor>) -> impl UiNode {
opacity_impl(child, alpha, true)
}
/// impl any filter, may need layout or not.
fn filter_any(child: impl UiNode, filter: impl IntoVar<Filter>, target_child: bool) -> impl UiNode {
let filter = filter.into_var();
let mut render_filter = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&filter);
render_filter = filter.with(Filter::try_render);
}
UiNodeOp::Update { .. } => {
filter.with_new(|f| {
if let Some(f) = f.try_render() {
render_filter = Some(f);
WIDGET.render();
} else {
render_filter = None;
WIDGET.layout();
}
});
}
UiNodeOp::Layout { .. } => {
filter.with(|f| {
if f.needs_layout() {
let f = Some(f.layout());
if render_filter != f {
render_filter = f;
WIDGET.render();
}
}
});
}
UiNodeOp::Render { frame } => {
if target_child {
frame.push_filter(MixBlendMode::Normal.into(), render_filter.as_ref().unwrap(), |frame| {
child.render(frame)
});
} else {
frame.push_inner_filter(render_filter.clone().unwrap(), |frame| child.render(frame));
}
}
_ => {}
})
}
/// impl any backdrop filter, may need layout or not.
fn backdrop_filter_any(child: impl UiNode, filter: impl IntoVar<Filter>) -> impl UiNode {
let filter = filter.into_var();
let mut render_filter = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&filter);
render_filter = filter.with(Filter::try_render);
}
UiNodeOp::Update { .. } => {
filter.with_new(|f| {
if let Some(f) = f.try_render() {
render_filter = Some(f);
WIDGET.render();
} else {
render_filter = None;
WIDGET.layout();
}
});
}
UiNodeOp::Layout { .. } => {
filter.with(|f| {
if f.needs_layout() {
let f = Some(f.layout());
if render_filter != f {
render_filter = f;
WIDGET.render();
}
}
});
}
UiNodeOp::Render { frame } => {
frame.push_inner_backdrop_filter(render_filter.clone().unwrap(), |frame| child.render(frame));
}
_ => {}
})
}
/// impl filters that need layout.
fn filter_layout(child: impl UiNode, filter: impl IntoVar<Filter>, target_child: bool) -> impl UiNode {
let filter = filter.into_var();
let mut render_filter = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&filter);
}
UiNodeOp::Layout { .. } => {
filter.with(|f| {
if f.needs_layout() {
let f = Some(f.layout());
if render_filter != f {
render_filter = f;
WIDGET.render();
}
}
});
}
UiNodeOp::Render { frame } => {
if target_child {
frame.push_filter(MixBlendMode::Normal.into(), render_filter.as_ref().unwrap(), |frame| {
child.render(frame)
});
} else {
frame.push_inner_filter(render_filter.clone().unwrap(), |frame| child.render(frame));
}
}
_ => {}
})
}
/// impl backdrop filters that need layout.
fn backdrop_filter_layout(child: impl UiNode, filter: impl IntoVar<Filter>) -> impl UiNode {
let filter = filter.into_var();
let mut render_filter = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&filter);
}
UiNodeOp::Layout { .. } => {
filter.with(|f| {
if f.needs_layout() {
let f = Some(f.layout());
if render_filter != f {
render_filter = f;
WIDGET.render();
}
}
});
}
UiNodeOp::Render { frame } => {
frame.push_inner_backdrop_filter(render_filter.clone().unwrap(), |frame| child.render(frame));
}
_ => {}
})
}
/// impl filters that only need render.
fn filter_render(child: impl UiNode, filter: impl IntoVar<Filter>, target_child: bool) -> impl UiNode {
let filter = filter.into_var().map(|f| f.try_render().unwrap());
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&filter);
}
UiNodeOp::Render { frame } => {
if target_child {
filter.with(|f| {
frame.push_filter(MixBlendMode::Normal.into(), f, |frame| child.render(frame));
});
} else {
frame.push_inner_filter(filter.get(), |frame| child.render(frame));
}
}
_ => {}
})
}
/// impl backdrop filter that only need render.
fn backdrop_filter_render(child: impl UiNode, filter: impl IntoVar<Filter>) -> impl UiNode {
let filter = filter.into_var().map(|f| f.try_render().unwrap());
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&filter);
}
UiNodeOp::Render { frame } => {
frame.push_inner_backdrop_filter(filter.get(), |frame| child.render(frame));
}
_ => {}
})
}
fn opacity_impl(child: impl UiNode, alpha: impl IntoVar<Factor>, target_child: bool) -> impl UiNode {
let frame_key = FrameValueKey::new_unique();
let alpha = alpha.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render_update(&alpha);
}
UiNodeOp::Render { frame } => {
let opacity = frame_key.bind_var(&alpha, |f| f.0);
if target_child {
frame.push_opacity(opacity, |frame| child.render(frame));
} else {
frame.push_inner_opacity(opacity, |frame| child.render(frame));
}
}
UiNodeOp::RenderUpdate { update } => {
update.update_f32_opt(frame_key.update_var(&alpha, |f| f.0));
child.render_update(update);
}
_ => {}
})
}
/// Sets how the widget blends with the parent widget.
#[property(CONTEXT, default(MixBlendMode::default()))]
pub fn mix_blend(child: impl UiNode, mode: impl IntoVar<MixBlendMode>) -> impl UiNode {
let mode = mode.into_var();
match_node(child, move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&mode);
}
UiNodeOp::Render { frame } => {
frame.push_inner_blend(mode.get().into(), |frame| c.render(frame));
}
_ => {}
})
}
/// Sets how the widget's child content blends with the widget.
#[property(CHILD_CONTEXT, default(MixBlendMode::default()))]
pub fn child_mix_blend(child: impl UiNode, mode: impl IntoVar<MixBlendMode>) -> impl UiNode {
let mode = mode.into_var();
match_node(child, move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&mode);
}
UiNodeOp::Render { frame } => {
frame.push_filter(mode.get().into(), &vec![], |frame| c.render(frame));
}
_ => {}
})
}

View File

@ -0,0 +1,18 @@
[package]
name = "zero-ui-wgt-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-wgt = { path = "../zero-ui-wgt" }
zero-ui-wgt-transform = { path = "../zero-ui-wgt-transform" }
zero-ui-wgt-fill = { path = "../zero-ui-wgt-fill" }
zero-ui-ext-input = { path = "../zero-ui-ext-input" }
zero-ui-ext-window = { path = "../zero-ui-ext-window" }
zero-ui-app = { path = "../zero-ui-app" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-ext-clipboard = { path = "../zero-ui-ext-clipboard" }
parking_lot = "0.12"

View File

@ -0,0 +1,248 @@
use parking_lot::Mutex;
use zero_ui_ext_input::{mouse::MOUSE_INPUT_EVENT, touch::TOUCH_INPUT_EVENT};
use zero_ui_wgt::prelude::*;
use std::sync::Arc;
pub use zero_ui_ext_input::pointer_capture::{CaptureMode, POINTER_CAPTURE};
/// Capture mouse and touch for the widget on press.
///
/// The capture happens only if any mouse button or touch is pressed on the window and the `mode` is [`Widget`] or [`Subtree`].
///
/// Captures are released when all mouse buttons and touch contacts stop being pressed on the window.
/// The capture is also released back to window if the `mode` changes to [`Window`].
///
/// # Examples
///
/// ```
/// # macro_rules! _demo { () => {
/// #[widget($crate::Button)]
/// pub struct Button(Container);
/// impl Button {
/// fn widget_intrinsic(&mut self) {
/// widget_set! {
/// self;
/// // Mouse does not interact with other widgets when pressed in the widget.
/// capture_pointer = true; //true == CaptureMode::Widget;
/// }
/// }
/// }
/// # }}
/// ```
///
/// [`Widget`]: CaptureMode::Widget
/// [`Subtree`]: CaptureMode::Subtree
/// [`Window`]: CaptureMode::Window
#[property(CONTEXT, default(false))]
pub fn capture_pointer(child: impl UiNode, mode: impl IntoVar<CaptureMode>) -> impl UiNode {
let mode = mode.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_event(&MOUSE_INPUT_EVENT).sub_event(&TOUCH_INPUT_EVENT).sub_var(&mode);
}
UiNodeOp::Event { update } => {
let mut capture = false;
if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
capture = args.is_mouse_down();
} else if let Some(args) = TOUCH_INPUT_EVENT.on(update) {
capture = args.is_touch_start();
}
if capture {
let widget_id = WIDGET.id();
match mode.get() {
CaptureMode::Widget => {
POINTER_CAPTURE.capture_widget(widget_id);
}
CaptureMode::Subtree => {
POINTER_CAPTURE.capture_subtree(widget_id);
}
CaptureMode::Window => (),
}
}
}
UiNodeOp::Update { .. } => {
if let Some(new_mode) = mode.get_new() {
let tree = WINDOW.info();
let widget_id = WIDGET.id();
if tree.get(widget_id).map(|w| w.interactivity().is_enabled()).unwrap_or(false) {
if let Some(current) = POINTER_CAPTURE.current_capture().get() {
if current.target.widget_id() == widget_id {
// If mode updated and we are capturing the mouse:
match new_mode {
CaptureMode::Widget => POINTER_CAPTURE.capture_widget(widget_id),
CaptureMode::Subtree => POINTER_CAPTURE.capture_subtree(widget_id),
CaptureMode::Window => POINTER_CAPTURE.release_capture(),
}
}
}
}
}
}
_ => {}
})
}
/// Capture mouse and touch for the widget on init.
///
/// The capture happens only if any mouse button or touch is pressed on the window and the `mode` is [`Widget`] or [`Subtree`].
///
/// Pointer captures are released when all mouse buttons stop being pressed on the window.
/// The capture is also released back to window if the `mode` changes to [`Window`] when the mouse is captured for the widget.
///
/// [`Widget`]: CaptureMode::Widget
/// [`Subtree`]: CaptureMode::Subtree
/// [`Window`]: CaptureMode::Window
#[property(CONTEXT, default(false))]
pub fn capture_pointer_on_init(child: impl UiNode, mode: impl IntoVar<CaptureMode>) -> impl UiNode {
let mode = mode.into_var();
let mut capture = true;
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&mode);
capture = true; // wait for info
}
UiNodeOp::Info { .. } => {
if std::mem::take(&mut capture) {
let widget_id = WIDGET.id();
match mode.get() {
CaptureMode::Widget => {
POINTER_CAPTURE.capture_widget(widget_id);
}
CaptureMode::Subtree => {
POINTER_CAPTURE.capture_subtree(widget_id);
}
CaptureMode::Window => (),
}
}
}
UiNodeOp::Update { .. } => {
if let Some(new_mode) = mode.get_new() {
let tree = WINDOW.info();
let widget_id = WIDGET.id();
if tree.get(widget_id).map(|w| w.interactivity().is_enabled()).unwrap_or(false) {
if let Some(current) = POINTER_CAPTURE.current_capture().get() {
if current.target.widget_id() == widget_id {
// If mode updated and we are capturing the mouse:
match new_mode {
CaptureMode::Widget => POINTER_CAPTURE.capture_widget(widget_id),
CaptureMode::Subtree => POINTER_CAPTURE.capture_subtree(widget_id),
CaptureMode::Window => POINTER_CAPTURE.release_capture(),
}
}
}
}
}
}
_ => {}
})
}
/// Only allow interaction inside the widget, descendants and ancestors.
///
/// When modal mode is enabled in a widget only it and widget descendants [allows interaction], all other widgets behave as if disabled, but
/// without the visual indication of disabled. This property is a building block for modal overlay widgets.
///
/// Only one widget can be the modal at a time, if multiple widgets set `modal = true` only the last one by traversal order is modal.
///
/// This property also sets the accessibility modal flag.
///
/// [allows interaction]: zero_ui_app::widget::info::WidgetInfo::interactivity
#[property(CONTEXT, default(false))]
pub fn modal(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
static MODAL_WIDGETS: StaticStateId<Arc<Mutex<ModalWidgetsData>>> = StaticStateId::new_unique();
#[derive(Default)]
struct ModalWidgetsData {
widgets: IdSet<WidgetId>,
last_in_tree: Option<WidgetId>,
}
let enabled = enabled.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&enabled);
WINDOW.init_state_default(&MODAL_WIDGETS); // insert window state
}
UiNodeOp::Deinit => {
let mws = WINDOW.req_state(&MODAL_WIDGETS);
// maybe unregister.
let mut mws = mws.lock();
let widget_id = WIDGET.id();
if mws.widgets.remove(&widget_id) && mws.last_in_tree == Some(widget_id) {
mws.last_in_tree = None;
}
}
UiNodeOp::Info { info } => {
let mws = WINDOW.req_state(&MODAL_WIDGETS);
if enabled.get() {
if let Some(mut a) = info.access() {
a.flag_modal();
}
let insert_filter = {
let mut mws = mws.lock();
if mws.widgets.insert(WIDGET.id()) {
mws.last_in_tree = None;
mws.widgets.len() == 1
} else {
false
}
};
if insert_filter {
// just registered and we are the first, insert the filter:
info.push_interactivity_filter(clmv!(mws, |a| {
let mut mws = mws.lock();
// caches the top-most modal.
if mws.last_in_tree.is_none() {
match mws.widgets.len() {
0 => unreachable!(),
1 => {
// only one modal
mws.last_in_tree = mws.widgets.iter().next().copied();
}
_ => {
// multiple modals, find the *top* one.
let mut found = 0;
for info in a.info.root().self_and_descendants() {
if mws.widgets.contains(&info.id()) {
mws.last_in_tree = Some(info.id());
found += 1;
if found == mws.widgets.len() {
break;
}
}
}
}
};
}
// filter, only allows inside self inclusive, and ancestors.
let modal = mws.last_in_tree.unwrap();
if a.info.self_and_ancestors().any(|w| w.id() == modal) || a.info.self_and_descendants().any(|w| w.id() == modal) {
Interactivity::ENABLED
} else {
Interactivity::BLOCKED
}
}));
}
} else {
// maybe unregister.
let mut mws = mws.lock();
let widget_id = WIDGET.id();
if mws.widgets.remove(&widget_id) && mws.last_in_tree == Some(widget_id) {
mws.last_in_tree = None;
}
}
}
_ => {}
})
}

View File

@ -0,0 +1,118 @@
//! Common commands.
//!
use zero_ui_ext_clipboard::{COPY_CMD, CUT_CMD, PASTE_CMD};
use zero_ui_wgt::prelude::*;
command! {
/// Represents the **new** action.
///
/// The command parameter can identify the new item type, otherwise the default (or single) type
/// must be used.
pub static NEW_CMD = {
name: "New",
shortcut: [shortcut!(CTRL+'N'), shortcut!(New)],
};
/// Represents the **open** action.
///
/// The command parameter can be an item path to open (like a `PathBuf`), otherwise the
/// command implementer must identify the item, either by context or by prompting the user.
pub static OPEN_CMD = {
name: "Open…",
shortcut: [shortcut!(CTRL+'O'), shortcut!(Open)],
};
/// Represents the **save** action.
///
/// Usually this saves to the already defined item path (open or previous save path),
/// otherwise the user is prompted like [`SAVE_AS_CMD`].
pub static SAVE_CMD = {
name: "Save",
shortcut: [shortcut!(CTRL+'S'), shortcut!(Save)],
};
/// Represents the **save-as** action.
///
/// Usually this prompts the user for a save path, even if a previous path is already known.
pub static SAVE_AS_CMD = {
name: "Save As…",
shortcut: [shortcut!(CTRL|SHIFT+'S')],
};
/// Represents the **context menu open** action.
pub static CONTEXT_MENU_CMD = {
shortcut: [shortcut!(SHIFT+F10), shortcut!(ContextMenu)],
};
}
command_property! {
/// New command handler.
///
/// Receives [`NEW_CMD`] command events scoped on the widget. The command parameter can be
/// the new item type identifier.
pub fn new {
cmd: NEW_CMD.scoped(WIDGET.id()),
}
/// Open command handler.
///
/// Receives [`OPEN_CMD`] command events scoped on the widget. The command parameter can be
/// a path to open, otherwise the path must be derived from context or the user prompted.
///
/// You can use [`WINDOWS.native_file_dialog`] to prompt the user for a file or folder path.
///
/// [`WINDOWS.native_file_dialog`]: WINDOWS::native_file_dialog
pub fn open {
cmd: OPEN_CMD.scoped(WIDGET.id()),
}
/// Save command handler.
///
/// Receives [`SAVE_CMD`] command events scoped on the widget. Usually saves to the last
/// open or save path, otherwise prompt the user like [`on_save_as`].
///
/// You can use [`WINDOWS.native_file_dialog`] to prompt the user for a file or folder path.
///
/// [`WINDOWS.native_file_dialog`]: WINDOWS::native_file_dialog
/// [`on_save_as`]: fn@on_save_as
pub fn save {
cmd: SAVE_CMD.scoped(WIDGET.id()),
}
/// Save-As command handler.
///
/// Receives [`SAVE_AS_CMD`] command events scoped on the widget. Usually prompts the user for
/// a new save path.
///
/// You can use [`WINDOWS.native_file_dialog`] to prompt the user for a file or folder path.
///
/// [`WINDOWS.native_file_dialog`]: WINDOWS::native_file_dialog
pub fn save_as {
cmd: SAVE_AS_CMD.scoped(WIDGET.id()),
}
/// Cut command handler.
///
/// Receives [`CUT_CMD`] command events scoped on the widget. You can use the [`CLIPBOARD`] service
/// to send data to the clipboard.
pub fn cut {
cmd: CUT_CMD.scoped(WIDGET.id()),
}
/// Paste command handler.
///
/// Receives [`COPY_CMD`] command events scoped on the widget. You can use the [`CLIPBOARD`] service
/// to send data to the clipboard.
pub fn copy {
cmd: COPY_CMD.scoped(WIDGET.id()),
}
/// Paste command handler.
///
/// Receives [`PASTE_CMD`] command events scoped on the widget. You can use the [`CLIPBOARD`] service
/// to receive data from the clipboard.
pub fn paste {
cmd: PASTE_CMD.scoped(WIDGET.id()),
}
}

View File

@ -0,0 +1,532 @@
//! Keyboard focus properties, [`tab_index`](fn@tab_index), [`focusable`](fn@focusable),
//! [`on_focus`](fn@on_focus), [`is_focused`](fn@is_focused) and more.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use zero_ui_ext_input::focus::*;
use zero_ui_ext_input::gesture::{CLICK_EVENT, GESTURES};
use zero_ui_ext_input::mouse::MOUSE_INPUT_EVENT;
use zero_ui_wgt::prelude::*;
/// Enables a widget to receive focus.
#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
pub fn focusable(child: impl UiNode, focusable: impl IntoVar<bool>) -> impl UiNode {
let focusable = focusable.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&focusable);
}
UiNodeOp::Info { info } => {
FocusInfoBuilder::new(info).focusable(focusable.get());
}
_ => {}
})
}
/// Customizes the widget order during TAB navigation.
#[property(CONTEXT, default(TabIndex::default()))]
pub fn tab_index(child: impl UiNode, tab_index: impl IntoVar<TabIndex>) -> impl UiNode {
let tab_index = tab_index.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&tab_index);
}
UiNodeOp::Info { info } => {
FocusInfoBuilder::new(info).tab_index(tab_index.get());
}
_ => {}
})
}
/// Widget is a focus scope.
#[property(CONTEXT, default(false))]
pub fn focus_scope(child: impl UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
focus_scope_impl(child, is_scope, false)
}
/// Widget is the ALT focus scope.
///
/// ALT focus scopes are also, `TabIndex::SKIP`, `skip_directional_nav`, `TabNav::Cycle` and `DirectionalNav::Cycle` by default.
///
/// Also see [`focus_click_behavior`] that can be used to return focus automatically when any widget inside the ALT scope
/// handles a click.
///
/// [`focus_click_behavior`]: fn@focus_click_behavior
#[property(CONTEXT, default(false))]
pub fn alt_focus_scope(child: impl UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
focus_scope_impl(child, is_scope, true)
}
fn focus_scope_impl(child: impl UiNode, is_scope: impl IntoVar<bool>, is_alt: bool) -> impl UiNode {
let is_scope = is_scope.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&is_scope);
}
UiNodeOp::Info { info } => {
let mut info = FocusInfoBuilder::new(info);
if is_alt {
info.alt_scope(is_scope.get());
} else {
info.scope(is_scope.get());
}
}
UiNodeOp::Deinit => {
if is_alt && FOCUS.is_focus_within(WIDGET.id()).get() {
// focus auto recovery can't return focus if the entire scope is missing.
FOCUS.focus_exit();
}
}
_ => {}
})
}
/// Behavior of a focus scope when it receives direct focus.
#[property(CONTEXT, default(FocusScopeOnFocus::default()))]
pub fn focus_scope_behavior(child: impl UiNode, behavior: impl IntoVar<FocusScopeOnFocus>) -> impl UiNode {
let behavior = behavior.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&behavior);
}
UiNodeOp::Info { info } => {
FocusInfoBuilder::new(info).on_focus(behavior.get());
}
_ => {}
})
}
/// Tab navigation within this focus scope.
#[property(CONTEXT, default(TabNav::Continue))]
pub fn tab_nav(child: impl UiNode, tab_nav: impl IntoVar<TabNav>) -> impl UiNode {
let tab_nav = tab_nav.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&tab_nav);
}
UiNodeOp::Info { info } => {
FocusInfoBuilder::new(info).tab_nav(tab_nav.get());
}
_ => {}
})
}
/// Arrows navigation within this focus scope.
#[property(CONTEXT, default(DirectionalNav::Continue))]
pub fn directional_nav(child: impl UiNode, directional_nav: impl IntoVar<DirectionalNav>) -> impl UiNode {
let directional_nav = directional_nav.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&directional_nav);
}
UiNodeOp::Info { info } => {
FocusInfoBuilder::new(info).directional_nav(directional_nav.get());
}
_ => {}
})
}
/// Keyboard shortcuts that focus this widget or its first focusable descendant or its first focusable parent.
#[property(CONTEXT, default(Shortcuts::default()))]
pub fn focus_shortcut(child: impl UiNode, shortcuts: impl IntoVar<Shortcuts>) -> impl UiNode {
let shortcuts = shortcuts.into_var();
let mut _handle = None;
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&shortcuts);
let s = shortcuts.get();
_handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
}
UiNodeOp::Update { .. } => {
if let Some(s) = shortcuts.get_new() {
_handle = Some(GESTURES.focus_shortcut(s, WIDGET.id()));
}
}
_ => {}
})
}
/// If directional navigation from outside this widget skips over it and its descendants.
///
/// Setting this to `true` is the directional navigation equivalent of setting `tab_index` to `SKIP`.
#[property(CONTEXT, default(false))]
pub fn skip_directional(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&enabled);
}
UiNodeOp::Info { info } => {
FocusInfoBuilder::new(info).skip_directional(enabled.get());
}
_ => {}
})
}
/// Behavior of an widget when a click event is send to it or a descendant.
///
/// See [`focus_click_behavior`] for more details.
///
/// [`focus_click_behavior`]: fn@focus_click_behavior
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FocusClickBehavior {
/// Click event always ignored.
Ignore,
/// Exit focus if a click event was send to the widget or descendant.
Exit,
/// Exit focus if a click event was send to the enabled widget or enabled descendant.
ExitEnabled,
/// Exit focus if the click event was received by the widget or descendant and event propagation was stopped.
ExitHandled,
}
impl std::fmt::Debug for FocusClickBehavior {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
write!(f, "FocusClickBehavior::")?;
}
match self {
Self::Ignore => write!(f, "Ignore"),
Self::Exit => write!(f, "Exit"),
Self::ExitEnabled => write!(f, "ExitEnabled"),
Self::ExitHandled => write!(f, "ExitHandled"),
}
}
}
/// Behavior of an widget when a click event is send to it or a descendant.
///
/// When a click event targets the widget or descendant the `behavior` closest to the target is applied,
/// that is if `Exit` is set in a parent, but `Ignore` is set on the target than the click is ignored.
/// This can be used to create a effects like a menu that closes on click for command items, but not for clicks
/// in sub-menu items.
///
/// Note that this property does not subscribe to any event, it only observes events flowing trough.
#[property(CONTEXT, default(FocusClickBehavior::Ignore))]
pub fn focus_click_behavior(child: impl UiNode, behavior: impl IntoVar<FocusClickBehavior>) -> impl UiNode {
let behavior = behavior.into_var();
match_node(child, move |c, op| {
if let UiNodeOp::Event { update } = op {
let mut delegate = || {
if let Some(ctx) = &*FOCUS_CLICK_HANDLED_CTX.get() {
c.event(update);
ctx.swap(true, Ordering::Relaxed)
} else {
let mut ctx = Some(Arc::new(Some(AtomicBool::new(false))));
FOCUS_CLICK_HANDLED_CTX.with_context(&mut ctx, || c.event(update));
let ctx = ctx.unwrap();
(*ctx).as_ref().unwrap().load(Ordering::Relaxed)
}
};
if let Some(args) = CLICK_EVENT.on(update) {
if !delegate() {
let exit = match behavior.get() {
FocusClickBehavior::Ignore => false,
FocusClickBehavior::Exit => true,
FocusClickBehavior::ExitEnabled => args.target.interactivity().is_enabled(),
FocusClickBehavior::ExitHandled => args.propagation().is_stopped(),
};
if exit {
FOCUS.focus_exit();
}
}
} else if let Some(args) = MOUSE_INPUT_EVENT.on_unhandled(update) {
if args.propagation().is_stopped() && !delegate() {
// CLICK_EVENT not send if source mouse-input is already handled.
let exit = match behavior.get() {
FocusClickBehavior::Ignore => false,
FocusClickBehavior::Exit => true,
FocusClickBehavior::ExitEnabled => args.target.interactivity().is_enabled(),
FocusClickBehavior::ExitHandled => true,
};
if exit {
FOCUS.focus_exit();
}
}
}
}
})
}
context_local! {
static FOCUS_CLICK_HANDLED_CTX: Option<AtomicBool> = None;
}
event_property! {
/// Focus changed in the widget or its descendants.
pub fn focus_changed {
event: FOCUS_CHANGED_EVENT,
args: FocusChangedArgs,
}
/// Widget got direct keyboard focus.
pub fn focus {
event: FOCUS_CHANGED_EVENT,
args: FocusChangedArgs,
filter: |args| args.is_focus(WIDGET.id()),
}
/// Widget lost direct keyboard focus.
pub fn blur {
event: FOCUS_CHANGED_EVENT,
args: FocusChangedArgs,
filter: |args| args.is_blur(WIDGET.id()),
}
/// Widget or one of its descendants got focus.
pub fn focus_enter {
event: FOCUS_CHANGED_EVENT,
args: FocusChangedArgs,
filter: |args| args.is_focus_enter(WIDGET.id()),
}
/// Widget or one of its descendants lost focus.
pub fn focus_leave {
event: FOCUS_CHANGED_EVENT,
args: FocusChangedArgs,
filter: |args| args.is_focus_leave(WIDGET.id()),
}
}
/// If the widget has keyboard focus.
///
/// This is only `true` if the widget itself is focused.
/// You can use [`is_focus_within`] to include focused widgets inside this one.
///
/// # Highlighting
///
/// This property is always `true` when the widget has focus, ignoring what device was used to move the focus,
/// usually when the keyboard is used a special visual indicator is rendered, a dotted line border is common,
/// this state is called *highlighting* and is tracked by the focus manager. To implement such a visual you can use the
/// [`is_focused_hgl`] property.
///
/// # Return Focus
///
/// Usually widgets that have a visual state for this property also have one for [`is_return_focus`], a common example is the
/// *text-input* or *text-box* widget that shows an emphasized border and blinking cursor when focused and still shows the
/// emphasized border without cursor when a menu is open and it is only the return focus.
///
/// [`is_focus_within`]: fn@zero_ui::properties::focus::is_focus_within
/// [`is_focused_hgl`]: fn@zero_ui::properties::focus::is_focused_hgl
/// [`is_return_focus`]: fn@zero_ui::properties::focus::is_return_focus
#[property(CONTEXT, widget_impl(FocusableMix<P>))]
pub fn is_focused(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
let id = WIDGET.id();
if args.is_focus(id) {
Some(true)
} else if args.is_blur(id) {
Some(false)
} else {
None
}
})
}
/// If the widget or one of its descendants has keyboard focus.
///
/// To check if only the widget has keyboard focus use [`is_focused`].
///
/// To track *highlighted* focus within use [`is_focus_within_hgl`] property.
///
/// [`is_focused`]: fn@zero_ui::properties::focus::is_focused
/// [`is_focus_within_hgl`]: fn@zero_ui::properties::focus::is_focus_within_hgl
#[property(CONTEXT, widget_impl(FocusableMix<P>))]
pub fn is_focus_within(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
let id = WIDGET.id();
if args.is_focus_enter(id) {
Some(true)
} else if args.is_focus_leave(id) {
Some(false)
} else {
None
}
})
}
/// If the widget has keyboard focus and the user is using the keyboard to navigate.
///
/// This is only `true` if the widget itself is focused and the focus was acquired by keyboard navigation.
/// You can use [`is_focus_within_hgl`] to include widgets inside this one.
///
/// # Highlighting
///
/// Usually when the keyboard is used to move the focus a special visual indicator is rendered, a dotted line border is common,
/// this state is called *highlighting* and is tracked by the focus manager, this property is only `true`.
///
/// [`is_focus_within_hgl`]: fn@zero_ui::properties::focus::is_focus_within_hgl
/// [`is_focused`]: fn@zero_ui::properties::focus::is_focused
#[property(CONTEXT, widget_impl(FocusableMix<P>))]
pub fn is_focused_hgl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
let id = WIDGET.id();
if args.is_focus(id) {
Some(args.highlight)
} else if args.is_blur(id) {
Some(false)
} else if args.is_hightlight_changed() && args.new_focus.as_ref().map(|p| p.widget_id() == id).unwrap_or(false) {
Some(args.highlight)
} else {
None
}
})
}
/// If the widget or one of its descendants has keyboard focus and the user is using the keyboard to navigate.
///
/// To check if only the widget has keyboard focus use [`is_focused_hgl`].
///
/// Also see [`is_focus_within`] to check if the widget has focus within regardless of highlighting.
///
/// [`is_focused_hgl`]: fn@zero_ui::properties::focus::is_focused_hgl
/// [`is_focus_within`]: fn@zero_ui::properties::focus::is_focus_within
#[property(CONTEXT, widget_impl(FocusableMix<P>))]
pub fn is_focus_within_hgl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, FOCUS_CHANGED_EVENT, |args| {
let id = WIDGET.id();
if args.is_focus_enter(id) {
Some(args.highlight)
} else if args.is_focus_leave(id) {
Some(false)
} else if args.is_hightlight_changed() && args.new_focus.as_ref().map(|p| p.contains(id)).unwrap_or(false) {
Some(args.highlight)
} else {
None
}
})
}
/// If the widget will be focused when a parent scope is focused.
///
/// Focus scopes can be configured to remember the last focused widget inside then, the focus than *returns* to
/// this widget when the scope receives focus. Alt scopes also remember the widget from which the *alt* focus happened
/// and can also return focus back to that widget.
///
/// Usually input widgets that have a visual state for [`is_focused`] also have a visual for this, a common example is the
/// *text-input* or *text-box* widget that shows an emphasized border and blinking cursor when focused and still shows the
/// emphasized border without cursor when a menu is open and it is only the return focus.
///
/// Note that a widget can be [`is_focused`] and `is_return_focus`, this property is `true` if any focus scope considers the
/// widget its return focus, you probably want to declare the widget visual states in such a order that [`is_focused`] overrides
/// the state of this property.
///
/// [`is_focused`]: fn@zero_ui::properties::focus::is_focused_hgl
/// [`is_focused_hgl`]: fn@zero_ui::properties::focus::is_focused_hgl
#[property(CONTEXT, widget_impl(FocusableMix<P>))]
pub fn is_return_focus(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
let id = WIDGET.id();
if args.is_return_focus(id) {
Some(true)
} else if args.was_return_focus(id) {
Some(false)
} else {
None
}
})
}
/// If the widget or one of its descendants will be focused when a focus scope is focused.
///
/// To check if only the widget is the return focus use [`is_return_focus`].
///
/// [`is_return_focus`]: fn@zero_ui::properties::focus::is_return_focus
#[property(CONTEXT, widget_impl(FocusableMix<P>))]
pub fn is_return_focus_within(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, RETURN_FOCUS_CHANGED_EVENT, |args| {
let id = WIDGET.id();
if args.is_return_focus_enter(id) {
Some(true)
} else if args.is_return_focus_leave(id) {
Some(false)
} else {
None
}
})
}
/// If the widget is focused on info init.
///
/// When the widget is inited and present in the info tree a [`FOCUS.focus_widget_or_related`] request is made for the widget.
#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
pub fn focus_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
enum State {
WaitInfo,
InfoInited,
Done,
}
let mut state = State::WaitInfo;
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
if enabled.get() {
state = State::WaitInfo;
} else {
state = State::Done;
}
}
UiNodeOp::Info { .. } => {
if let State::WaitInfo = &state {
state = State::InfoInited;
// next update will be after the info is in tree.
WIDGET.update();
}
}
UiNodeOp::Update { .. } => {
if let State::InfoInited = &state {
state = State::Done;
FOCUS.focus_widget_or_related(WIDGET.id(), false, false);
}
}
_ => {}
})
}
/// Focusable widget mixin. Enables keyboard focusing on the widget and adds a focused highlight visual.
#[widget_mixin]
pub struct FocusableMix<P>(P);
impl<P: WidgetImpl> FocusableMix<P> {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
focusable = true;
when *#is_focused_hgl {
zero_ui_wgt_fill::foreground_highlight = {
offsets: FOCUS_HIGHLIGHT_OFFSETS_VAR,
widths: FOCUS_HIGHLIGHT_WIDTHS_VAR,
sides: FOCUS_HIGHLIGHT_SIDES_VAR,
};
}
}
}
}
context_var! {
/// Padding offsets of the foreground highlight when the widget is focused.
pub static FOCUS_HIGHLIGHT_OFFSETS_VAR: SideOffsets = 1;
/// Border widths of the foreground highlight when the widget is focused.
pub static FOCUS_HIGHLIGHT_WIDTHS_VAR: SideOffsets = 0.5;
/// Border sides of the foreground highlight when the widget is focused.
pub static FOCUS_HIGHLIGHT_SIDES_VAR: BorderSides = BorderSides::dashed(rgba(200, 200, 200, 1.0));
}
/// Sets the foreground highlight values used when the widget is focused and highlighted.
#[property(
CONTEXT,
default(FOCUS_HIGHLIGHT_OFFSETS_VAR, FOCUS_HIGHLIGHT_WIDTHS_VAR, FOCUS_HIGHLIGHT_SIDES_VAR),
widget_impl(FocusableMix<P>)
)]
pub fn focus_highlight(
child: impl UiNode,
offsets: impl IntoVar<SideOffsets>,
widths: impl IntoVar<SideOffsets>,
sides: impl IntoVar<BorderSides>,
) -> impl UiNode {
let child = with_context_var(child, FOCUS_HIGHLIGHT_WIDTHS_VAR, offsets);
let child = with_context_var(child, FOCUS_HIGHLIGHT_OFFSETS_VAR, widths);
with_context_var(child, FOCUS_HIGHLIGHT_SIDES_VAR, sides)
}

View File

@ -0,0 +1,168 @@
//! Gesture events and control, [`on_click`](fn@on_click), [`click_shortcut`](fn@click_shortcut) and more.
//!
//! These events aggregate multiple lower-level events to represent a user interaction.
//! Prefer using these events over the events directly tied to an input device.
use zero_ui_app::shortcut::Shortcuts;
use zero_ui_ext_input::gesture::{ShortcutClick, CLICK_EVENT, GESTURES};
use zero_ui_view_api::access::AccessCmdName;
use zero_ui_wgt::prelude::*;
pub use zero_ui_ext_input::gesture::ClickArgs;
event_property! {
/// On widget click from any source and of any click count and the widget is enabled.
///
/// This is the most general click handler, it raises for all possible sources of the [`CLICK_EVENT`] and any number
/// of consecutive clicks. Use [`click`](fn@click) to handle only primary button clicks or [`on_any_single_click`](fn@on_any_single_click)
/// to not include double/triple clicks.
pub fn any_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_enabled(WIDGET.id()),
with: access_click,
}
/// On widget click from any source and of any click count and the widget is disabled.
pub fn disabled_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_disabled(WIDGET.id()),
with: access_click,
}
/// On widget click from any source but excluding double/triple clicks and the widget is enabled.
///
/// This raises for all possible sources of [`CLICK_EVENT`], but only when the click count is one. Use
/// [`on_single_click`](fn@on_single_click) to handle only primary button clicks.
pub fn any_single_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_single() && args.is_enabled(WIDGET.id()),
with: access_click,
}
/// On widget click from any source but exclusive double-clicks and the widget is enabled.
///
/// This raises for all possible sources of [`CLICK_EVENT`], but only when the click count is two. Use
/// [`on_double_click`](fn@on_double_click) to handle only primary button clicks.
pub fn any_double_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_double() && args.is_enabled(WIDGET.id()),
}
/// On widget click from any source but exclusive triple-clicks and the widget is enabled.
///
/// This raises for all possible sources of [`CLICK_EVENT`], but only when the click count is three. Use
/// [`on_triple_click`](fn@on_triple_click) to handle only primary button clicks.
pub fn any_triple_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_triple() && args.is_enabled(WIDGET.id()),
}
/// On widget click with the primary button and any click count and the widget is enabled.
///
/// This raises only if the click [is primary](ClickArgs::is_primary), but raises for any click count (double/triple clicks).
/// Use [`on_any_click`](fn@on_any_click) to handle clicks from any button or [`on_single_click`](fn@on_single_click) to not include
/// double/triple clicks.
pub fn click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_primary() && args.is_enabled(WIDGET.id()),
with: access_click,
}
/// On widget click with the primary button, excluding double/triple clicks and the widget is enabled.
///
/// This raises only if the click [is primary](ClickArgs::is_primary) and the click count is one. Use
/// [`on_any_single_click`](fn@on_any_single_click) to handle single clicks from any button.
pub fn single_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_primary() && args.is_single() && args.is_enabled(WIDGET.id()),
with: access_click,
}
/// On widget click with the primary button and exclusive double-clicks and the widget is enabled.
///
/// This raises only if the click [is primary](ClickArgs::is_primary) and the click count is two. Use
/// [`on_any_double_click`](fn@on_any_double_click) to handle double clicks from any button.
pub fn double_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_primary() && args.is_double() && args.is_enabled(WIDGET.id()),
}
/// On widget click with the primary button and exclusive triple-clicks and the widget is enabled.
///
/// This raises only if the click [is primary](ClickArgs::is_primary) and the click count is three. Use
/// [`on_any_double_click`](fn@on_any_double_click) to handle double clicks from any button.
pub fn triple_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_primary() && args.is_triple() && args.is_enabled(WIDGET.id()),
}
/// On widget click with the secondary/context button and the widget is enabled.
///
/// This raises only if the click [is context](ClickArgs::is_context).
pub fn context_click {
event: CLICK_EVENT,
args: ClickArgs,
filter: |args| args.is_context() && args.is_enabled(WIDGET.id()),
with: access_click,
}
}
/// Keyboard shortcuts that focus and clicks this widget.
///
/// When any of the `shortcuts` is pressed, focus and click this widget.
#[property(CONTEXT)]
pub fn click_shortcut(child: impl UiNode, shortcuts: impl IntoVar<Shortcuts>) -> impl UiNode {
click_shortcut_node(child, shortcuts, ShortcutClick::Primary)
}
/// Keyboard shortcuts that focus and [context clicks](fn@on_context_click) this widget.
///
/// When any of the `shortcuts` is pressed, focus and context clicks this widget.
#[property(CONTEXT)]
pub fn context_click_shortcut(child: impl UiNode, shortcuts: impl IntoVar<Shortcuts>) -> impl UiNode {
click_shortcut_node(child, shortcuts, ShortcutClick::Context)
}
fn click_shortcut_node(child: impl UiNode, shortcuts: impl IntoVar<Shortcuts>, kind: ShortcutClick) -> impl UiNode {
let shortcuts = shortcuts.into_var();
let mut _handle = None;
match_node(child, move |_, op| {
let new = match op {
UiNodeOp::Init => {
WIDGET.sub_var(&shortcuts);
Some(shortcuts.get())
}
UiNodeOp::Deinit => {
_handle = None;
None
}
UiNodeOp::Update { .. } => shortcuts.get_new(),
_ => None,
};
if let Some(s) = new {
_handle = Some(GESTURES.click_shortcut(s, kind, WIDGET.id()));
}
})
}
pub(crate) fn access_click(child: impl UiNode, _: bool) -> impl UiNode {
access_capable(child, AccessCmdName::Click)
}
fn access_capable(child: impl UiNode, cmd: AccessCmdName) -> impl UiNode {
match_node(child, move |_, op| {
if let UiNodeOp::Info { info } = op {
if let Some(mut access) = info.access() {
access.push_command(cmd)
}
}
})
}

View File

@ -0,0 +1,110 @@
//! Keyboard events, [`on_key_down`](fn@on_key_down), [`on_key_up`](fn@on_key_up) and more.
//!
//! These events are low level and directly tied to a keyboard device.
//! Before using them review the [`gesture`](super::gesture) properties, in particular
//! the [`click_shortcut`](fn@super::gesture::click_shortcut) property.
use zero_ui_ext_input::keyboard::{KeyInputArgs, KeyState, KEY_INPUT_EVENT};
use zero_ui_wgt::prelude::*;
event_property! {
/// Event fired when a keyboard key is pressed or released and the widget is enabled.
///
/// # Route
///
/// The event is raised in the [keyboard focused](crate::properties::is_focused)
/// widget and then each parent up to the root. If [`propagation`](EventArgs::propagation) stop
/// is requested the event is not notified further. If the widget is disabled or blocked the event is not notified.
///
/// This route is also called *bubbling*.
///
/// # Keys
///
/// Any key press/release generates a key input event, including keys that don't map
/// to any virtual key, see [`KeyInputArgs`] for more details. To take text input use [`on_char_input`] instead.
/// For key combinations consider using [`on_shortcut`].
///
/// # Underlying Event
///
/// This event property uses the [`KeyInputEvent`] that is included in the default app.
pub fn key_input {
event: KEY_INPUT_EVENT,
args: KeyInputArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Event fired when a keyboard key is pressed or released and the widget is disabled.
///
/// # Route
///
/// The event is raised in the [keyboard focused](crate::properties::is_focused)
/// widget and then each parent up to the root. If [`propagation`](EventArgs::propagation) stop
/// is requested the event is not notified further. If the widget is enabled or blocked the event is not notified.
///
/// This route is also called *bubbling*.
///
/// # Keys
///
/// Any key press/release generates a key input event, including keys that don't map
/// to any virtual key, see [`KeyInputArgs`] for more details. To take text input use [`on_char_input`] instead.
/// For key combinations consider using [`on_shortcut`].
///
/// # Underlying Event
///
/// This event property uses the [`KeyInputEvent`] that is included in the default app.
pub fn disabled_key_input {
event: KEY_INPUT_EVENT,
args: KeyInputArgs,
filter: |args| args.is_disabled(WIDGET.id()),
}
/// Event fired when a keyboard key is pressed and the widget is enabled.
///
/// # Route
///
/// The event is raised in the [keyboard focused](crate::properties::is_focused)
/// widget and then each parent up to the root. If [`propagation`](EventArgs::propagation) stop
/// is requested the event is not notified further. If the widget is disabled or blocked the event is not notified.
///
/// This route is also called *bubbling*.
///
/// # Keys
///
/// Any key press generates a key down event, including keys that don't map to any virtual key, see [`KeyInputArgs`]
/// for more details. To take text input use [`on_char_input`] instead.
/// For key combinations consider using [`on_shortcut`].
///
/// # Underlying Event
///
/// This event property uses the [`KeyInputEvent`] that is included in the default app.
pub fn key_down {
event: KEY_INPUT_EVENT,
args: KeyInputArgs,
filter: |args| args.state == KeyState::Pressed && args.is_enabled(WIDGET.id()),
}
/// Event fired when a keyboard key is released and the widget is enabled.
///
/// # Route
///
/// The event is raised in the [keyboard focused](crate::properties::is_focused)
/// widget and then each parent up to the root. If [`propagation`](EventArgs::propagation) stop
/// is requested the event is not notified further. If the widget is disabled or blocked the event is not notified.
///
/// This route is also called *bubbling*.
///
/// # Keys
///
/// Any key release generates a key up event, including keys that don't map to any virtual key, see [`KeyInputArgs`]
/// for more details. To take text input use [`on_char_input`] instead.
/// For key combinations consider using [`on_shortcut`].
///
/// # Underlying Event
///
/// This event property uses the [`KeyInputEvent`] that is included in the default app.
pub fn key_up {
event: KEY_INPUT_EVENT,
args: KeyInputArgs,
filter: |args| args.state == KeyState::Released && args.is_enabled(WIDGET.id()),
}
}

View File

@ -0,0 +1,21 @@
//! Input events and focus properties.
pub mod commands;
pub mod focus;
pub mod gesture;
pub mod keyboard;
pub mod mouse;
pub mod pointer_capture;
pub mod touch;
mod capture;
pub use capture::*;
mod misc;
pub use misc::*;
mod state;
pub use state::*;
mod touch_props;
pub use touch_props::*;

View File

@ -0,0 +1,79 @@
use zero_ui_ext_input::mouse::{ClickMode, WidgetInfoBuilderMouseExt as _, MOUSE_HOVERED_EVENT};
use zero_ui_ext_window::WINDOW_Ext as _;
use zero_ui_wgt::prelude::*;
pub use zero_ui_view_api::window::CursorIcon;
/// Widget property that sets the [`CursorIcon`] displayed when hovering the widget.
///
/// # Examples
///
/// ```
/// # use zero_ui::prelude::*;
/// Container! {
/// cursor = CursorIcon::Pointer;
/// child = Text!("Mouse over this text shows the hand cursor");
/// }
/// # ;
/// ```
///
/// [`CursorIcon`]: crate::core::window::CursorIcon
#[property(CONTEXT, default(CursorIcon::Default))]
pub fn cursor(child: impl UiNode, cursor: impl IntoVar<Option<CursorIcon>>) -> impl UiNode {
let cursor = cursor.into_var();
let mut hovered_binding = None;
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_event(&MOUSE_HOVERED_EVENT);
}
UiNodeOp::Deinit => {
hovered_binding = None;
}
UiNodeOp::Event { update } => {
if let Some(args) = MOUSE_HOVERED_EVENT.on(update) {
let is_over = args.target.as_ref().map(|t| t.as_path().contains(WIDGET.id())).unwrap_or(false);
if is_over {
if hovered_binding.is_none() {
// we are not already set, setup binding.
let c = WINDOW.vars().cursor();
c.set_from(&cursor);
hovered_binding = Some(cursor.bind(&c));
}
} else {
// restore to default, if not set to other value already
if hovered_binding.is_some() {
hovered_binding = None;
let value = cursor.get();
WINDOW.vars().cursor().modify(move |c| {
if c.as_ref() == &value {
*c.to_mut() = Some(CursorIcon::Default);
}
});
}
}
}
}
_ => {}
})
}
/// Defines how click events are generated for the widget.
///
/// Setting this to `None` will cause the widget to inherit the parent mode, or [`ClickMode::default()`] if
/// no parent sets the click mode.
#[property(CONTEXT, default(None))]
pub fn click_mode(child: impl UiNode, mode: impl IntoVar<Option<ClickMode>>) -> impl UiNode {
let mode = mode.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&mode);
}
UiNodeOp::Info { info } => {
info.set_click_mode(mode.get());
}
_ => {}
})
}

View File

@ -0,0 +1,184 @@
//! Mouse events, [`on_mouse_move`](fn@on_mouse_move), [`on_mouse_enter`](fn@on_mouse_enter),
//! [`on_mouse_down`](fn@on_mouse_down) and more.
//!
//! There events are low level and directly tied to a mouse device.
//! Before using them review the [`gesture`](super::gesture) events, in particular the
//! [`on_click`](fn@super::gesture::on_click) event.
use zero_ui_ext_input::mouse::{
MouseClickArgs, MouseHoverArgs, MouseInputArgs, MouseMoveArgs, MouseWheelArgs, MOUSE_CLICK_EVENT, MOUSE_HOVERED_EVENT,
MOUSE_INPUT_EVENT, MOUSE_MOVE_EVENT, MOUSE_WHEEL_EVENT,
};
use zero_ui_wgt::prelude::*;
event_property! {
/// Mouse cursor moved over the widget and cursor capture allows it.
pub fn mouse_move {
event: MOUSE_MOVE_EVENT,
args: MouseMoveArgs,
filter: |args| args.capture_allows(),
}
/// Mouse button pressed or released while the cursor is over the widget, the widget is enabled and no cursor
/// capture blocks it.
pub fn mouse_input {
event: MOUSE_INPUT_EVENT,
args: MouseInputArgs,
filter: |args| args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Mouse button pressed or release while the cursor is over the widget, the widget is disabled and no cursor
/// capture blocks it.
pub fn disabled_mouse_input {
event: MOUSE_INPUT_EVENT,
args: MouseInputArgs,
filter: |args| args.is_disabled(WIDGET.id()) && args.capture_allows(),
}
/// Mouse button pressed while the cursor is over the widget, the widget is enabled and cursor capture allows it.
pub fn mouse_down {
event: MOUSE_INPUT_EVENT,
args: MouseInputArgs,
filter: |args| args.is_mouse_down() && args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Mouse button released while the cursor if over the widget, the widget is enabled and cursor capture allows it.
pub fn mouse_up {
event: MOUSE_INPUT_EVENT,
args: MouseInputArgs,
filter: |args| args.is_mouse_up() && args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Mouse clicked on the widget with any button and including double+ clicks and the widget is enabled.
pub fn mouse_any_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Mouse clicked on the widget with any button and including double+ clicks and the widget is disabled.
pub fn disabled_mouse_any_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_disabled(WIDGET.id()),
}
/// Mouse clicked on the widget with any button but excluding double+ clicks and the widget is enabled.
pub fn mouse_any_single_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_single() && args.is_enabled(WIDGET.id()),
}
/// Mouse double clicked on the widget with any button and the widget is enabled.
pub fn mouse_any_double_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_double() && args.is_enabled(WIDGET.id()),
}
/// Mouse triple clicked on the widget with any button and the widget is enabled.
pub fn mouse_any_triple_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_triple() && args.is_enabled(WIDGET.id()),
}
/// Mouse clicked on the widget with the primary button including double+ clicks and the widget is enabled.
pub fn mouse_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_primary() && args.is_enabled(WIDGET.id()),
}
/// Mouse clicked on the widget with the primary button including double+ clicks and the widget is disabled.
pub fn disabled_mouse_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_primary() && args.is_disabled(WIDGET.id()),
}
/// Mouse clicked on the widget with the primary button excluding double+ clicks and the widget is enabled.
pub fn mouse_single_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_primary() && args.is_single() && args.is_enabled(WIDGET.id()),
}
/// Mouse double clicked on the widget with the primary button and the widget is enabled.
pub fn mouse_double_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_primary() && args.is_double() && args.is_enabled(WIDGET.id()),
}
/// Mouse triple clicked on the widget with the primary button and the widget is enabled.
pub fn mouse_triple_click {
event: MOUSE_CLICK_EVENT,
args: MouseClickArgs,
filter: |args| args.is_primary() && args.is_triple() && args.is_enabled(WIDGET.id()),
}
/// Mouse is now over the widget or a descendant widget, the widget is enabled and cursor capture allows it.
pub fn mouse_enter {
event: MOUSE_HOVERED_EVENT,
args: MouseHoverArgs,
filter: |args| args.is_mouse_enter_enabled(),
}
/// Mouse is no longer over the widget or any descendant widget, the widget is enabled and cursor capture allows it.
pub fn mouse_leave {
event: MOUSE_HOVERED_EVENT,
args: MouseHoverArgs,
filter: |args| args.is_mouse_leave_enabled(),
}
/// Mouse entered or left the widget and descendant widgets area, the widget is enabled and cursor capture allows it.
///
/// You can use the [`is_mouse_enter`] and [`is_mouse_leave`] methods to determinate the state change.
///
/// [`is_mouse_enter`]: MouseHoverArgs::is_mouse_enter
/// [`is_mouse_leave`]: MouseHoverArgs::is_mouse_leave
pub fn mouse_hovered {
event: MOUSE_HOVERED_EVENT,
args: MouseHoverArgs,
filter: |args| args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Mouse entered or left the widget and descendant widgets area, the widget is disabled and cursor capture allows it.
pub fn disabled_mouse_hovered {
event: MOUSE_HOVERED_EVENT,
args: MouseHoverArgs,
filter: |args| args.is_disabled(WIDGET.id()) && args.capture_allows(),
}
/// Mouse wheel scrolled while pointer is hovering widget and the widget is enabled.
pub fn mouse_wheel {
event: MOUSE_WHEEL_EVENT,
args: MouseWheelArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Mouse wheel scrolled while pointer is hovering widget and the widget is disabled.
pub fn disabled_mouse_wheel {
event: MOUSE_WHEEL_EVENT,
args: MouseWheelArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Mouse wheel scrolled while pointer is hovering the widget and the pressed keyboard modifiers allow a scroll operation and
/// the widget is enabled.
pub fn mouse_scroll {
event: MOUSE_WHEEL_EVENT,
args: MouseWheelArgs,
filter: |args| args.is_scroll() && args.is_enabled(WIDGET.id()),
}
/// Mouse wheel scrolled while pointer is hovering the widget and the pressed keyboard modifiers allow a zoom operation and
/// the widget is enabled.
pub fn mouse_zoom {
event: MOUSE_WHEEL_EVENT,
args: MouseWheelArgs,
filter: |args| args.is_zoom() && args.is_enabled(WIDGET.id()),
}
}

View File

@ -0,0 +1,26 @@
//! Mouse and touch capture events.
use zero_ui_ext_input::pointer_capture::{PointerCaptureArgs, POINTER_CAPTURE_EVENT};
use zero_ui_wgt::prelude::*;
event_property! {
/// Widget acquired mouse and touch capture.
pub fn got_pointer_capture {
event: POINTER_CAPTURE_EVENT,
args: PointerCaptureArgs,
filter: |args| args.is_got(WIDGET.id()),
}
/// Widget lost mouse and touch capture.
pub fn lost_pointer_capture {
event: POINTER_CAPTURE_EVENT,
args: PointerCaptureArgs,
filter: |args| args.is_lost(WIDGET.id()),
}
/// Widget acquired or lost mouse and touch capture.
pub fn pointer_capture_changed {
event: POINTER_CAPTURE_EVENT,
args: PointerCaptureArgs,
}
}

View File

@ -0,0 +1,484 @@
use std::{collections::HashSet, time::Duration};
use zero_ui_app::timer::TIMERS;
use zero_ui_ext_input::{
gesture::{CLICK_EVENT, GESTURES},
mouse::{ClickMode, WidgetInfoMouseExt as _, MOUSE_HOVERED_EVENT, MOUSE_INPUT_EVENT},
pointer_capture::POINTER_CAPTURE_EVENT,
touch::TOUCHED_EVENT,
};
use zero_ui_view_api::{mouse::ButtonState, touch::TouchPhase};
use zero_ui_wgt::prelude::*;
/// If the mouse pointer is over the widget or a descendant and the widget is [`DISABLED`].
///
/// [`DISABLED`]: Interactivity::DISABLED
#[property(EVENT)]
pub fn is_hovered_disabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, MOUSE_HOVERED_EVENT, |args| {
if args.is_mouse_enter_disabled() {
Some(true)
} else if args.is_mouse_leave_disabled() {
Some(false)
} else {
None
}
})
}
/// If the mouse pointer is over the widget or a descendant and the widget is [`ENABLED`].
///
/// This state property does not consider pointer capture, if the pointer is captured by the widget
/// but is not actually over the widget this is `false`, use [`is_cap_hovered`] to include the captured state.
///
/// The value is always `false` when the widget is not [`ENABLED`], use [`is_hovered_disabled`] to implement *disabled hovered* visuals.
///
/// [`is_cap_hovered`]: fn@is_cap_hovered
/// [`ENABLED`]: Interactivity::ENABLED
/// [`is_hovered_disabled`]: fn@is_hovered_disabled
#[property(EVENT)]
pub fn is_hovered(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, MOUSE_HOVERED_EVENT, |args| {
if args.is_mouse_enter_enabled() {
Some(true)
} else if args.is_mouse_leave_enabled() {
Some(false)
} else {
None
}
})
}
/// If the mouse pointer is over the widget, or is over a widget descendant, or is captured by the widget.
///
/// The value is always `false` when the widget is not [`ENABLED`].
///
/// [`ENABLED`]: Interactivity::ENABLED
#[property(EVENT)]
pub fn is_cap_hovered(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state2(
child,
state,
false,
MOUSE_HOVERED_EVENT,
false,
|hovered_args| {
if hovered_args.is_mouse_enter_enabled() {
Some(true)
} else if hovered_args.is_mouse_leave_enabled() {
Some(false)
} else {
None
}
},
POINTER_CAPTURE_EVENT,
false,
|cap_args| {
if cap_args.is_got(WIDGET.id()) {
Some(true)
} else if cap_args.is_lost(WIDGET.id()) {
Some(false)
} else {
None
}
},
|hovered, captured| Some(hovered || captured),
)
}
/// If the mouse pointer is pressed in the widget and the widget is [`ENABLED`].
///
/// This is `true` when the mouse primary button started pressing in the widget
/// and the pointer is over the widget and the primary button is still pressed and
/// the widget is fully [`ENABLED`].
///
/// This state property only considers pointer capture for repeat [click modes](ClickMode), if the pointer is captured by a widget
/// with [`ClickMode::repeat`] `false` and the pointer is not actually over the widget the state is `false`,
/// use [`is_cap_mouse_pressed`] to always include the captured state.
///
/// [`ENABLED`]: Interactivity::ENABLED
/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
#[property(EVENT)]
pub fn is_mouse_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state3(
child,
state,
false,
MOUSE_HOVERED_EVENT,
false,
|hovered_args| {
if hovered_args.is_mouse_enter_enabled() {
Some(true)
} else if hovered_args.is_mouse_leave_enabled() {
Some(false)
} else {
None
}
},
MOUSE_INPUT_EVENT,
false,
|input_args| {
if input_args.is_primary() {
match input_args.state {
ButtonState::Pressed => {
if input_args.capture_allows() {
return Some(input_args.is_enabled(WIDGET.id()));
}
}
ButtonState::Released => return Some(false),
}
}
None
},
POINTER_CAPTURE_EVENT,
false,
|cap_args| {
if cap_args.is_got(WIDGET.id()) {
Some(true)
} else if cap_args.is_lost(WIDGET.id()) {
Some(false)
} else {
None
}
},
{
let mut info_gen = 0;
let mut mode = ClickMode::default();
move |hovered, is_down, is_captured| {
// cache mode
let tree = WINDOW.info();
if info_gen != tree.stats().generation {
mode = tree.get(WIDGET.id()).unwrap().click_mode();
info_gen = tree.stats().generation;
}
if mode.repeat {
Some(is_down || is_captured)
} else {
Some(hovered && is_down)
}
}
},
)
}
/// If the mouse pointer is pressed in the widget or was captured during press and the widget is [`ENABLED`].
///
/// [`ENABLED`]: Interactivity::ENABLED
#[property(EVENT)]
pub fn is_cap_mouse_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state2(
child,
state,
false,
MOUSE_INPUT_EVENT,
false,
|input_args| {
if input_args.is_primary() {
match input_args.state {
ButtonState::Pressed => {
if input_args.capture_allows() {
return Some(input_args.is_enabled(WIDGET.id()));
}
}
ButtonState::Released => return Some(false),
}
}
None
},
POINTER_CAPTURE_EVENT,
false,
|cap_args| {
if cap_args.is_got(WIDGET.id()) {
Some(true)
} else if cap_args.is_lost(WIDGET.id()) {
Some(false)
} else {
None
}
},
|is_down, is_captured| Some(is_down || is_captured),
)
}
/// If the widget was clicked by shortcut or accessibility event and the [`shortcut_pressed_duration`] has not elapsed.
///
/// [`shortcut_pressed_duration`]: GESTURES::shortcut_pressed_duration
#[property(EVENT)]
pub fn is_shortcut_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
let mut shortcut_press = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
let _ = state.set(false);
WIDGET.sub_event(&CLICK_EVENT);
}
UiNodeOp::Deinit => {
let _ = state.set(false);
}
UiNodeOp::Event { update } => {
if let Some(args) = CLICK_EVENT.on(update) {
if (args.is_from_keyboard() || args.is_from_access()) && args.is_enabled(WIDGET.id()) {
// if a shortcut click happened, we show pressed for the duration of `shortcut_pressed_duration`
// unless we where already doing that, then we just stop showing pressed, this causes
// a flickering effect when rapid clicks are happening.
if shortcut_press.take().is_none() {
let duration = GESTURES.shortcut_pressed_duration().get();
if duration != Duration::default() {
let dl = TIMERS.deadline(duration);
dl.subscribe(UpdateOp::Update, WIDGET.id()).perm();
shortcut_press = Some(dl);
let _ = state.set(true);
}
} else {
let _ = state.set(false);
}
}
}
}
UiNodeOp::Update { updates } => {
child.update(updates);
if let Some(timer) = &shortcut_press {
if timer.is_new() {
shortcut_press = None;
let _ = state.set(false);
}
}
}
_ => {}
})
}
/// If a touch contact point is over the widget or a descendant and the widget is [`ENABLED`].
///
/// This state property does not consider pointer capture, if the pointer is captured by the widget
/// but is not actually over the widget this is `false`, use [`is_cap_touched`] to include the captured state.
///
/// This state property also does not consider where the touch started, if it started in a different widget
/// and is not over this widget the widget is touched, use [`is_touched_from_start`] to ignore touched that move in.
///
/// The value is always `false` when the widget is not [`ENABLED`].
///
/// [`is_cap_touched`]: fn@is_cap_touched
/// [`is_touched_from_start`]: fn@is_touched_from_start
/// [`ENABLED`]: Interactivity::ENABLED
#[property(EVENT)]
pub fn is_touched(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state(child, state, false, TOUCHED_EVENT, |args| {
if args.is_touch_enter_enabled() {
Some(true)
} else if args.is_touch_leave_enabled() {
Some(false)
} else {
None
}
})
}
/// If a touch contact that started over the widget is over the widget and the widget is [`ENABLED`].
///
/// This state property does not consider pointer capture, if the pointer is captured by the widget
/// but is not actually over the widget this is `false`, use [`is_cap_touched_from_start`] to include the captured state.
///
/// The value is always `false` when the widget is not [`ENABLED`].
///
/// [`ENABLED`]: Interactivity::ENABLED
/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
#[property(EVENT)]
pub fn is_touched_from_start(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let mut touches_started = HashSet::new();
event_is_state(child, state, false, TOUCHED_EVENT, move |args| {
if args.is_touch_enter_enabled() {
match args.phase {
TouchPhase::Start => {
touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
touches_started.insert(args.touch_propagation.clone());
Some(true)
}
TouchPhase::Move => Some(touches_started.contains(&args.touch_propagation)),
TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
}
} else if args.is_touch_leave_enabled() {
if let TouchPhase::End | TouchPhase::Cancel = args.phase {
touches_started.remove(&args.touch_propagation);
}
Some(false)
} else {
None
}
})
}
/// If a touch contact point is over the widget, or is over a widget descendant, or is captured by the widget.
///
/// The value is always `false` when the widget is not [`ENABLED`].
///
/// [`ENABLED`]: Interactivity::ENABLED
#[property(EVENT)]
pub fn is_cap_touched(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
event_is_state2(
child,
state,
false,
TOUCHED_EVENT,
false,
|hovered_args| {
if hovered_args.is_touch_enter_enabled() {
Some(true)
} else if hovered_args.is_touch_leave_enabled() {
Some(false)
} else {
None
}
},
POINTER_CAPTURE_EVENT,
false,
|cap_args| {
if cap_args.is_got(WIDGET.id()) {
Some(true)
} else if cap_args.is_lost(WIDGET.id()) {
Some(false)
} else {
None
}
},
|hovered, captured| Some(hovered || captured),
)
}
/// If a touch contact point is over the widget, or is over a widget descendant, or is captured by the widget.
///
/// The value is always `false` when the widget is not [`ENABLED`].
///
/// [`ENABLED`]: Interactivity::ENABLED
#[property(EVENT)]
pub fn is_cap_touched_from_start(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let mut touches_started = HashSet::new();
event_is_state2(
child,
state,
false,
TOUCHED_EVENT,
false,
move |hovered_args| {
if hovered_args.is_touch_enter_enabled() {
match hovered_args.phase {
TouchPhase::Start => {
touches_started.retain(|t: &EventPropagationHandle| !t.is_stopped()); // for touches released outside the widget.
touches_started.insert(hovered_args.touch_propagation.clone());
Some(true)
}
TouchPhase::Move => Some(touches_started.contains(&hovered_args.touch_propagation)),
TouchPhase::End | TouchPhase::Cancel => Some(false), // weird
}
} else if hovered_args.is_touch_leave_enabled() {
if let TouchPhase::End | TouchPhase::Cancel = hovered_args.phase {
touches_started.remove(&hovered_args.touch_propagation);
}
Some(false)
} else {
None
}
},
POINTER_CAPTURE_EVENT,
false,
|cap_args| {
if cap_args.is_got(WIDGET.id()) {
Some(true)
} else if cap_args.is_lost(WIDGET.id()) {
Some(false)
} else {
None
}
},
|hovered, captured| Some(hovered || captured),
)
}
/// If [`is_mouse_pressed`] or [`is_touched_from_start`].
///
/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pointer_pressed`] to
/// include the captured state.
///
/// [`is_mouse_pressed`]: fn@is_mouse_pressed
/// [`is_touched_from_start`]: fn@is_touched_from_start
/// [`is_cap_pointer_pressed`]: fn@is_cap_pointer_pressed
#[property(EVENT)]
pub fn is_pointer_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let pressed = state_var();
let child = is_mouse_pressed(child, pressed.clone());
let touched = state_var();
let child = is_touched_from_start(child, touched.clone());
bind_is_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
}
/// If [`is_mouse_pressed`], [`is_touched_from_start`] or [`is_shortcut_pressed`].
///
/// Note that [`is_mouse_pressed`] and [`is_touched_from_start`] do not consider pointer capture, use [`is_cap_pressed`] to
/// include the captured state.
///
/// [`shortcut_pressed_duration`]: Gestures::shortcut_pressed_duration
/// [`is_mouse_pressed`]: fn@is_mouse_pressed
/// [`is_touched_from_start`]: fn@is_touched_from_start
/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
/// [`is_cap_pressed`]: fn@is_cap_pressed
#[property(EVENT)]
pub fn is_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let pressed = state_var();
let child = is_mouse_pressed(child, pressed.clone());
let touched = state_var();
let child = is_touched_from_start(child, touched.clone());
let shortcut_pressed = state_var();
let child = is_shortcut_pressed(child, shortcut_pressed.clone());
bind_is_state(
child,
merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
state,
)
}
/// If [`is_cap_mouse_pressed`] or [`is_cap_touched_from_start`].
///
/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
#[property(EVENT)]
pub fn is_cap_pointer_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let pressed = state_var();
let child = is_cap_mouse_pressed(child, pressed.clone());
let touched = state_var();
let child = is_cap_touched_from_start(child, touched.clone());
bind_is_state(child, merge_var!(pressed, touched, |&p, &t| p || t), state)
}
/// If [`is_cap_mouse_pressed`], [`is_cap_touched_from_start`] or [`is_shortcut_pressed`].
///
/// [`is_cap_mouse_pressed`]: fn@is_cap_mouse_pressed
/// [`is_cap_touched_from_start`]: fn@is_cap_touched_from_start
/// [`is_shortcut_pressed`]: fn@is_shortcut_pressed
#[property(EVENT)]
pub fn is_cap_pressed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let pressed = state_var();
let child = is_cap_mouse_pressed(child, pressed.clone());
let touched = state_var();
let child = is_cap_touched_from_start(child, touched.clone());
let shortcut_pressed = state_var();
let child = is_shortcut_pressed(child, pressed.clone());
bind_is_state(
child,
merge_var!(pressed, touched, shortcut_pressed, |&p, &t, &s| p || t || s),
state,
)
}

View File

@ -0,0 +1,119 @@
//! Touch events, [`on_touch_move`](fn@on_touch_move), [`on_touch_tap`](fn@on_touch_tap),
//! [`on_touch_start`](fn@on_touch_start) and more.
//!
//! There events are low level and directly tied to touch inputs.
//! Before using them review the [`gesture`](super::gesture) events, in particular the
//! [`on_click`](fn@super::gesture::on_click) event.
use zero_ui_ext_input::touch::{
TouchInputArgs, TouchLongPressArgs, TouchMoveArgs, TouchTapArgs, TouchTransformArgs, TouchedArgs, TOUCHED_EVENT, TOUCH_INPUT_EVENT,
TOUCH_LONG_PRESS_EVENT, TOUCH_MOVE_EVENT, TOUCH_TAP_EVENT, TOUCH_TRANSFORM_EVENT,
};
use zero_ui_wgt::prelude::*;
event_property! {
/// Touch contact moved over the widget and cursor capture allows it.
pub fn touch_move {
event: TOUCH_MOVE_EVENT,
args: TouchMoveArgs,
filter: |args| args.capture_allows(),
}
/// Touch contact started or ended over the widget, the widget is enabled and cursor capture allows it.
pub fn touch_input {
event: TOUCH_INPUT_EVENT,
args: TouchInputArgs,
filter: |args| args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Touch contact started or ended over the widget, the widget is disabled and cursor capture allows it.
pub fn disabled_touch_input {
event: TOUCH_INPUT_EVENT,
args: TouchInputArgs,
filter: |args| args.is_disabled(WIDGET.id()) && args.capture_allows(),
}
/// Touch contact started over the widget, the widget is enabled and cursor capture allows it.
pub fn touch_start {
event: TOUCH_INPUT_EVENT,
args: TouchInputArgs,
filter: |args| args.is_touch_start() && args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Touch contact ended over the widget, the widget is enabled and cursor capture allows it.
pub fn touch_end {
event: TOUCH_INPUT_EVENT,
args: TouchInputArgs,
filter: |args| args.is_touch_end() && args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Touch contact canceled over the widget, the widget is enabled and cursor capture allows it.
pub fn touch_cancel {
event: TOUCH_INPUT_EVENT,
args: TouchInputArgs,
filter: |args| args.is_touch_cancel() && args.is_enabled(WIDGET.id()) && args.capture_allows(),
}
/// Touch tap on the widget and the widget is enabled.
pub fn touch_tap {
event: TOUCH_TAP_EVENT,
args: TouchTapArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Touch tap on the widget and the widget is disabled.
pub fn disabled_touch_tap {
event: TOUCH_TAP_EVENT,
args: TouchTapArgs,
filter: |args| args.is_disabled(WIDGET.id()),
}
/// Touch contact is now over the widget or a descendant widget and the widget is enabled.
pub fn touch_enter {
event: TOUCHED_EVENT,
args: TouchedArgs,
filter: |args| args.is_touch_enter_enabled(),
}
/// Touch contact is no longer over the widget or any descendant widget and the widget is enabled.
pub fn touch_leave {
event: TOUCHED_EVENT,
args: TouchedArgs,
filter: |args| args.is_touch_leave_enabled(),
}
/// Touch contact entered or left the widget and descendant widgets area and the widget is enabled.
///
/// You can use the [`is_touch_enter`] and [`is_touch_leave`] methods to determinate the state change.
///
/// [`is_touch_enter`]: TouchedArgs::is_touch_enter
/// [`is_touch_leave`]: TouchedArgs::is_touch_leave
pub fn touched {
event: TOUCHED_EVENT,
args: TouchedArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Touch gesture to translate, scale or rotate happened over this widget.
pub fn touch_transform {
event: TOUCH_TRANSFORM_EVENT,
args: TouchTransformArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Single touch contact was made and held in place for a duration of time (default 500ms) on
/// the widget and the widget is enabled.
pub fn touch_long_press {
event: TOUCH_LONG_PRESS_EVENT,
args: TouchLongPressArgs,
filter: |args| args.is_enabled(WIDGET.id()),
}
/// Single touch contact was made and held in place for a duration of time (default 500ms) on
/// the widget and the widget is disabled.
pub fn disabled_touch_long_press {
event: TOUCH_LONG_PRESS_EVENT,
args: TouchLongPressArgs,
filter: |args| args.is_disabled(WIDGET.id()),
}
}

View File

@ -0,0 +1,65 @@
use zero_ui_ext_input::touch::{TouchTransformMode, TOUCH_TRANSFORM_EVENT};
use zero_ui_view_api::touch::TouchPhase;
use zero_ui_wgt::prelude::*;
/// Applies transforms from touch gestures on the widget.
#[property(LAYOUT, default(false))]
pub fn touch_transform(child: impl UiNode, mode: impl IntoVar<TouchTransformMode>) -> impl UiNode {
let mode = mode.into_var();
let mut handle = EventHandle::dummy();
let mut transform_committed = PxTransform::identity();
let mut transform = PxTransform::identity();
match_node(child, move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&mode);
if !mode.get().is_empty() {
handle = TOUCH_TRANSFORM_EVENT.subscribe(WIDGET.id());
}
}
UiNodeOp::Deinit => {
handle = EventHandle::dummy();
}
UiNodeOp::Event { update } => {
if let Some(args) = TOUCH_TRANSFORM_EVENT.on(update) {
if args.propagation().is_stopped() {
return;
}
let t = transform_committed.then(&args.local_transform(mode.get()));
if transform != t {
transform = t;
WIDGET.render_update();
}
match args.phase {
TouchPhase::Start | TouchPhase::Move => {}
TouchPhase::End => {
transform_committed = transform;
}
TouchPhase::Cancel => {
transform = transform_committed;
WIDGET.render_update();
}
}
}
}
UiNodeOp::Update { .. } => {
if let Some(mode) = mode.get_new() {
if handle.is_dummy() {
if !mode.is_empty() {
handle = TOUCH_TRANSFORM_EVENT.subscribe(WIDGET.id());
}
} else if mode.is_empty() {
handle = EventHandle::dummy();
}
}
}
UiNodeOp::Render { frame } => {
frame.push_inner_transform(&transform, |f| c.render(f));
}
UiNodeOp::RenderUpdate { update } => {
update.with_inner_transform(&transform, |u| c.render_update(u));
}
_ => {}
})
}

View File

@ -0,0 +1,16 @@
[package]
name = "zero-ui-wgt-inspector"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-ext-input = { path = "../zero-ui-ext-input" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-ext-window = { path = "../zero-ui-ext-window" }
zero-ui-units = { path = "../zero-ui-units" }
serde = { version = "1", features = ["derive"] }
tracing = "0.1"

View File

@ -0,0 +1,388 @@
//! Debug inspection properties.
use std::{cell::RefCell, fmt, rc::Rc};
use zero_ui_ext_input::{
focus::WidgetInfoFocusExt as _,
mouse::{MOUSE_HOVERED_EVENT, MOUSE_MOVE_EVENT},
};
use zero_ui_ext_window::WINDOW_Ext as _;
use zero_ui_units::Orientation2D;
use zero_ui_view_api::display_list::FrameValue;
use zero_ui_wgt::prelude::*;
/// Target of inspection properties.
#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum InspectMode {
/// Just the widget where the inspector property is set.
Widget,
/// The widget where the inspector property is set and all descendants.
///
/// This is the `true` value.
WidgetAndDescendants,
/// Disable inspection.
///
/// This is the `false` value.
#[default]
Disabled,
}
impl fmt::Debug for InspectMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "InspectMode::")?;
}
match self {
Self::Widget => write!(f, "Widget"),
Self::WidgetAndDescendants => write!(f, "WidgetAndDescendants"),
Self::Disabled => write!(f, "Disabled"),
}
}
}
impl_from_and_into_var! {
fn from(widget_and_descendants: bool) -> InspectMode {
if widget_and_descendants {
InspectMode::WidgetAndDescendants
} else {
InspectMode::Disabled
}
}
}
/// Draws a debug dot in target widget's [center point].
///
/// [center point]: crate::core::widget_info::WidgetInfo::center
#[property(CONTEXT, default(false))]
pub fn show_center_points(child: impl UiNode, mode: impl IntoVar<InspectMode>) -> impl UiNode {
show_widget_tree(
child,
|_, wgt, frame| {
frame.push_debug_dot(wgt.center(), colors::GREEN);
},
mode,
)
}
/// Draws a border for every target widget's outer and inner bounds.
#[property(CONTEXT, default(false))]
pub fn show_bounds(child: impl UiNode, mode: impl IntoVar<InspectMode>) -> impl UiNode {
show_widget_tree(
child,
|_, wgt, frame| {
let p = Dip::new(1).to_px(frame.scale_factor());
let outer_bounds = wgt.outer_bounds();
let inner_bounds = wgt.inner_bounds();
if outer_bounds != inner_bounds && !outer_bounds.is_empty() {
frame.push_border(
wgt.outer_bounds(),
PxSideOffsets::new_all_same(p),
BorderSides::dotted(web_colors::PINK),
PxCornerRadius::zero(),
);
}
if !inner_bounds.size.is_empty() {
frame.push_border(
inner_bounds,
PxSideOffsets::new_all_same(p),
BorderSides::solid(web_colors::ROYAL_BLUE),
PxCornerRadius::zero(),
);
}
},
mode,
)
}
/// Draws a border over every inlined widget row in the window.
#[property(CONTEXT, default(false))]
pub fn show_rows(child: impl UiNode, mode: impl IntoVar<InspectMode>) -> impl UiNode {
let spatial_id = SpatialFrameId::new_unique();
show_widget_tree(
child,
move |i, wgt, frame| {
let p = Dip::new(1).to_px(frame.scale_factor());
let wgt = wgt.bounds_info();
let transform = wgt.inner_transform();
if let Some(inline) = wgt.inline() {
frame.push_reference_frame((spatial_id, i as u32).into(), FrameValue::Value(transform), false, false, |frame| {
for row in &inline.rows {
if !row.size.is_empty() {
frame.push_border(
*row,
PxSideOffsets::new_all_same(p),
BorderSides::dotted(web_colors::LIGHT_SALMON),
PxCornerRadius::zero(),
);
}
}
})
};
},
mode,
)
}
fn show_widget_tree(
child: impl UiNode,
mut render: impl FnMut(usize, WidgetInfo, &mut FrameBuilder) + Send + 'static,
mode: impl IntoVar<InspectMode>,
) -> impl UiNode {
let mode = mode.into_var();
let cancel_space = SpatialFrameId::new_unique();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&mode);
}
UiNodeOp::Render { frame } => {
child.render(frame);
let mut r = |render: &mut dyn FnMut(WidgetInfo, &mut FrameBuilder)| {
let tree = WINDOW.info();
if let Some(wgt) = tree.get(WIDGET.id()) {
if WIDGET.parent_id().is_none() {
render(wgt, frame);
} else if let Some(t) = frame.transform().inverse() {
// cancel current transform
frame.push_reference_frame(cancel_space.into(), t.into(), false, false, |frame| {
render(wgt, frame);
})
} else {
tracing::error!("cannot inspect from `{:?}`, non-reversable transform", WIDGET.id())
}
}
};
match mode.get() {
InspectMode::Widget => {
r(&mut |wgt, frame| {
render(0, wgt, frame);
});
}
InspectMode::WidgetAndDescendants => {
r(&mut |wgt, frame| {
for (i, wgt) in wgt.self_and_descendants().enumerate() {
render(i, wgt, frame);
}
});
}
InspectMode::Disabled => {}
}
}
_ => {}
})
}
/// Draws the inner bounds that where tested for the mouse point.
///
/// # Window Only
///
/// This property only works if set in a window, if set in another widget it will log an error and don't render anything.
#[property(CONTEXT, default(false))]
pub fn show_hit_test(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
let mut handles = EventHandles::default();
let mut valid = false;
let mut fails = vec![];
let mut hits = vec![];
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
valid = WIDGET.parent_id().is_none();
if valid {
WIDGET.sub_var(&enabled);
if enabled.get() {
let id = WIDGET.id();
handles = [MOUSE_MOVE_EVENT.subscribe(id), MOUSE_HOVERED_EVENT.subscribe(id)].into();
} else {
handles.clear();
}
} else {
tracing::error!("properties that render widget info are only valid in a window");
}
}
UiNodeOp::Deinit => {
handles.clear();
}
UiNodeOp::Event { update } => {
if let Some(args) = MOUSE_MOVE_EVENT.on(update) {
if valid && enabled.get() {
let factor = WINDOW.vars().scale_factor().get();
let pt = args.position.to_px(factor);
let new_fails = Rc::new(RefCell::new(vec![]));
let new_hits = Rc::new(RefCell::new(vec![]));
let tree = WINDOW.info();
let _ = tree
.root()
.spatial_iter(clmv!(new_fails, new_hits, |w| {
let bounds = w.inner_bounds();
let hit = bounds.contains(pt);
if hit {
new_hits.borrow_mut().push(bounds);
} else {
new_fails.borrow_mut().push(bounds);
}
hit
}))
.count();
let new_fails = Rc::try_unwrap(new_fails).unwrap().into_inner();
let new_hits = Rc::try_unwrap(new_hits).unwrap().into_inner();
if fails != new_fails || hits != new_hits {
fails = new_fails;
hits = new_hits;
WIDGET.render();
}
}
} else if let Some(args) = MOUSE_HOVERED_EVENT.on(update) {
if args.target.is_none() && !fails.is_empty() && !hits.is_empty() {
fails.clear();
hits.clear();
WIDGET.render();
}
}
}
UiNodeOp::Update { .. } => {
if let Some(enabled) = enabled.get_new() {
if enabled && valid {
let id = WIDGET.id();
handles = [MOUSE_MOVE_EVENT.subscribe(id), MOUSE_HOVERED_EVENT.subscribe(id)].into();
} else {
handles.clear();
}
WIDGET.render();
}
}
UiNodeOp::Render { frame } => {
child.render(frame);
if valid && enabled.get() {
let widths = PxSideOffsets::new_all_same(Px(1));
let fail_sides = BorderSides::solid(colors::RED);
let hits_sides = BorderSides::solid(web_colors::LIME_GREEN);
frame.with_hit_tests_disabled(|frame| {
for fail in &fails {
if !fail.size.is_empty() {
frame.push_border(*fail, widths, fail_sides, PxCornerRadius::zero());
}
}
for hit in &hits {
if !hit.size.is_empty() {
frame.push_border(*hit, widths, hits_sides, PxCornerRadius::zero());
}
}
});
}
}
_ => {}
})
}
/// Draw the directional query for closest sibling of the hovered focusable widget.
///
/// # Window Only
///
/// This property only works if set in a window, if set in another widget it will log an error and don't render anything.
#[property(CONTEXT, default(None))]
pub fn show_directional_query(child: impl UiNode, orientation: impl IntoVar<Option<Orientation2D>>) -> impl UiNode {
let orientation = orientation.into_var();
let mut valid = false;
let mut search_quads = vec![];
let mut _mouse_hovered_handle = None;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
valid = WIDGET.parent_id().is_none();
if valid {
WIDGET.sub_var(&orientation);
if orientation.get().is_some() {
_mouse_hovered_handle = Some(MOUSE_HOVERED_EVENT.subscribe(WIDGET.id()));
}
} else {
tracing::error!("properties that render widget info are only valid in a window");
}
}
UiNodeOp::Deinit => {
_mouse_hovered_handle = None;
}
UiNodeOp::Event { update } => {
if !valid {
return;
}
if let Some(args) = MOUSE_HOVERED_EVENT.on(update) {
if let Some(orientation) = orientation.get() {
let mut none = true;
if let Some(target) = &args.target {
let tree = WINDOW.info();
for w_id in target.widgets_path().iter().rev() {
if let Some(w) = tree.get(*w_id) {
if let Some(w) = w.into_focusable(true, true) {
let sq: Vec<_> = orientation
.search_bounds(w.info().center(), Px::MAX, tree.spatial_bounds().to_box2d())
.map(|q| q.to_rect())
.collect();
if search_quads != sq {
search_quads = sq;
WIDGET.render();
}
none = false;
break;
}
}
}
}
if none && !search_quads.is_empty() {
search_quads.clear();
WIDGET.render();
}
}
}
}
UiNodeOp::Update { .. } => {
if !valid {
return;
}
if let Some(ori) = orientation.get_new() {
search_quads.clear();
if ori.is_some() {
_mouse_hovered_handle = Some(MOUSE_HOVERED_EVENT.subscribe(WIDGET.id()));
} else {
_mouse_hovered_handle = None;
}
WIDGET.render();
}
}
UiNodeOp::Render { frame } => {
child.render(frame);
if valid && orientation.get().is_some() {
let widths = PxSideOffsets::new_all_same(Px(1));
let quad_sides = BorderSides::solid(colors::YELLOW);
frame.with_hit_tests_disabled(|frame| {
for quad in &search_quads {
if !quad.size.is_empty() {
frame.push_border(*quad, widths, quad_sides, PxCornerRadius::zero());
}
}
});
}
}
_ => {}
})
}

View File

@ -0,0 +1,3 @@
//! Debug properties and inspector implementation.
pub mod debug;

View File

@ -0,0 +1,12 @@
[package]
name = "zero-ui-wgt-size_offset"
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-wgt = { path = "../zero-ui-wgt" }
euclid = "0.22"
serde = { version = "1", features = ["derive"] }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
[package]
name = "zero-ui-wgt-style"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-app = { path = "../zero-ui-app" }

View File

@ -0,0 +1,415 @@
//! Style building blocks.
// suppress nag about very simple boxed closure signatures.
#![allow(clippy::type_complexity)]
use zero_ui_app::widget::builder::Importance;
use zero_ui_wgt::prelude::*;
use std::sync::Arc;
use std::{fmt, ops};
/// Represents a set of properties that can be applied to any styleable widget.
///
/// This *widget* can be instantiated using the same syntax as any widget, but it produces a [`StyleBuilder`]
/// instance instead of an widget. Widgets that have [`StyleMix<P>`] can be modified using properties
/// defined in a style, the properties are dynamically spliced into each widget instance.
///
/// Styles must only visually affect the styled widget, this is a semantic distinction only, any property can be set
/// in a style, so feel free to setup event handlers in styles, but only if they are used to affect the widget visually.
///
/// # Derived Styles
///
/// Note that you can declare a custom style *widget* using the same inheritance mechanism of normal widgets, as long
/// as they build to [`StyleBuilder`].
#[widget($crate::Style)]
pub struct Style(WidgetBase);
impl Style {
/// Build the style.
pub fn widget_build(&mut self) -> StyleBuilder {
StyleBuilder::from_builder(self.widget_take())
}
}
/// Styleable widget mixin.
///
/// Widgets that inherit from this one have a `style_fn` property that can be set to a [`style_fn!`]
/// that generates properties that are dynamically injected into the widget to alter its appearance.
///
/// The style mixin drastically affects the widget build process, only the `style_fn` property and `when` condition
/// properties that affects it are instantiated with the widget, all the other properties and intrinsic nodes are instantiated
/// on init, after the style is generated.
///
/// Styleable widgets usually have a more elaborate style setup that supports mixing multiple contextual styles, see
/// [`with_style_extension`] for a full styleable widget example.
#[widget_mixin]
pub struct StyleMix<P>(P);
impl<P: WidgetImpl> StyleMix<P> {
fn widget_intrinsic(&mut self) {
self.base().widget_builder().set_custom_build(StyleMix::<()>::custom_build);
}
}
impl<P> StyleMix<P> {
/// The custom build that is set on intrinsic by the mixin.
pub fn custom_build(mut wgt: WidgetBuilder) -> BoxedUiNode {
// 1 - "split_off" the property `style_fn`
// this moves the property and any `when` that affects it to a new widget builder.
let style_id = property_id!(style_fn);
let mut style_builder = WidgetBuilder::new(wgt.widget_type());
wgt.split_off([style_id, style_id], &mut style_builder);
if style_builder.has_properties() {
// 2.a - There was a `style_fn` property, build a "mini widget" that is only the style property
// and when condition properties that affect it.
#[cfg(trace_widget)]
wgt.push_build_action(|wgt| {
// avoid double trace as the style builder already inserts a widget tracer.
wgt.disable_trace_widget();
});
let mut wgt = Some(wgt);
style_builder.push_build_action(move |b| {
// 3 - The actual StyleNode and builder is a child of the "mini widget".
let style = b.capture_var::<StyleFn>(style_id).unwrap();
b.set_child(style_node(None, wgt.take().unwrap(), style));
});
// 4 - Build the "mini widget",
// if the `style` property was not affected by any `when` this just returns the `StyleNode`.
style_builder.build()
} else {
// 2.b - There was no `style_fn` property, this widget is not styleable, just build the default.
wgt.build()
}
}
}
/// Replaces the widget's style with an style function.
///
/// Properties and `when` conditions in the generated style are applied to the widget as
/// if they where set on it. Note that changing the style causes the widget info tree to rebuild,
/// prefer property binding and `when` conditions to cause visual changes that happen often.
///
/// The style property it-self can be affected by `when` conditions set on the widget, this works to a limited
/// extent as only the style and when condition properties is loaded to evaluate, so a when condition that depends
/// on the full widget context will not work.
///
/// You can also set this property to an style instance directly, it will always work when you have an instance
/// of the style per widget instance, but if the style is used in multiple widgets properties with cloneable
/// values will be cloned, properties with node values will be moved to the last usage place, breaking the style
/// in previous instances. When in doubt use [`style_fn!`], it always works.
///
/// Is `nil` by default.
#[property(WIDGET, capture, default(StyleFn::nil()), widget_impl(StyleMix<P>))]
pub fn style_fn(style: impl IntoVar<StyleFn>) {}
/// Helper for declaring properties that [extend] a style set from a context var.
///
/// [extend]: StyleFn::with_extend
///
/// # Examples
///
/// Example styleable widget defining a `foo::vis::extend_style` property that extends the contextual style.
///
/// ```
/// # fn main() { }
/// use zero_ui::prelude::new_widget::*;
///
/// #[widget($crate::Foo)]
/// pub struct Foo(StyleMix<WidgetBase>);
/// impl Foo {
///
/// fn widget_intrinsic(&mut self) {
/// widget_set! {
/// self;
/// style_fn = STYLE_VAR;
/// }
/// }
/// }
///
/// context_var! {
/// /// Foo style.
/// pub static STYLE_VAR: StyleFn = style_fn!(|_args| {
/// Style! {
/// background_color = color_scheme_pair((colors::BLACK, colors::WHITE));
/// cursor = CursorIcon::Crosshair;
/// }
/// });
///}
///
/// /// Replace the contextual [`STYLE_VAR`] with `style`.
/// #[property(CONTEXT, default(STYLE_VAR))]
/// pub fn replace_style(
/// child: impl UiNode,
/// style: impl IntoVar<StyleFn>
/// ) -> impl UiNode {
/// with_context_var(child, STYLE_VAR, style)
/// }
///
/// /// Extends the contextual [`STYLE_VAR`] with the `style` override.
/// #[property(CONTEXT, default(StyleFn::nil()))]
/// pub fn extend_style(
/// child: impl UiNode,
/// style: impl IntoVar<StyleFn>
/// ) -> impl UiNode {
/// style::with_style_extension(child, STYLE_VAR, style)
/// }
/// ```
pub fn with_style_extension(child: impl UiNode, style_context: ContextVar<StyleFn>, extension: impl IntoVar<StyleFn>) -> impl UiNode {
with_context_var(
child,
style_context,
merge_var!(style_context, extension.into_var(), |base, over| {
base.clone().with_extend(over.clone())
}),
)
}
fn style_node(child: Option<BoxedUiNode>, builder: WidgetBuilder, style: BoxedVar<StyleFn>) -> impl UiNode {
match_node_typed(child, move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&style);
if let Some(style) = style.get().call(&StyleArgs {}) {
let mut builder = builder.clone();
builder.extend(style.into_builder());
*c.child() = Some(builder.default_build());
} else {
*c.child() = Some(builder.clone().default_build());
}
}
UiNodeOp::Deinit => {
c.deinit();
*c.child() = None;
}
UiNodeOp::Update { .. } => {
if style.is_new() {
WIDGET.reinit();
WIDGET.update_info().layout().render();
c.delegated();
}
}
_ => {}
})
}
/// Represents a style instance.
///
/// Use the [`Style!`] *widget* to declare.
///
/// [`Style!`]: struct@Style
#[derive(Debug, Clone)]
pub struct StyleBuilder {
builder: WidgetBuilder,
}
impl Default for StyleBuilder {
fn default() -> Self {
Self {
builder: WidgetBuilder::new(Style::widget_type()),
}
}
}
impl StyleBuilder {
/// Importance of style properties set by default in style widgets.
///
/// Is `Importance::WIDGET - 10`.
pub const WIDGET_IMPORTANCE: Importance = Importance(Importance::WIDGET.0 - 10);
/// Importance of style properties set in style instances.
///
/// Is `Importance::INSTANCE - 10`.
pub const INSTANCE_IMPORTANCE: Importance = Importance(Importance::INSTANCE.0 - 10);
/// Negative offset on the position index of style properties.
///
/// Is `1`.
pub const POSITION_OFFSET: u16 = 1;
/// New style from a widget builder.
///
/// The importance and position index of properties are adjusted,
/// any custom build or widget build action is ignored.
pub fn from_builder(mut wgt: WidgetBuilder) -> StyleBuilder {
wgt.clear_build_actions();
wgt.clear_custom_build();
for p in wgt.properties_mut() {
*p.importance = match *p.importance {
Importance::WIDGET => StyleBuilder::WIDGET_IMPORTANCE,
Importance::INSTANCE => StyleBuilder::INSTANCE_IMPORTANCE,
other => other,
};
p.position.index = p.position.index.saturating_sub(Self::POSITION_OFFSET);
}
StyleBuilder { builder: wgt }
}
/// Unwrap the style dynamic widget.
pub fn into_builder(self) -> WidgetBuilder {
self.builder
}
/// Overrides `self` with `other`.
pub fn extend(&mut self, other: StyleBuilder) {
self.builder.extend(other.builder);
}
/// If the style does nothing.
pub fn is_empty(&self) -> bool {
!self.builder.has_properties() && !self.builder.has_whens() && !self.builder.has_unsets()
}
}
impl From<StyleBuilder> for WidgetBuilder {
fn from(t: StyleBuilder) -> Self {
t.into_builder()
}
}
impl From<WidgetBuilder> for StyleBuilder {
fn from(p: WidgetBuilder) -> Self {
StyleBuilder::from_builder(p)
}
}
impl_from_and_into_var! {
/// Singleton.
fn from(style: StyleBuilder) -> StyleFn {
StyleFn::singleton(style)
}
}
/// Arguments for [`StyleFn`] closure.
///
/// Empty in this version.
#[derive(Debug)]
pub struct StyleArgs {}
/// Boxed shared closure that generates a style instance for a given widget context.
///
/// You can also use the [`style_fn!`] macro, it has the advantage of being clone move.
#[derive(Clone)]
pub struct StyleFn(Option<Arc<dyn Fn(&StyleArgs) -> Option<StyleBuilder> + Send + Sync>>);
impl Default for StyleFn {
fn default() -> Self {
Self::nil()
}
}
impl PartialEq for StyleFn {
// can only fail by returning `false` in some cases where the value pointer is actually equal.
// see: https://github.com/rust-lang/rust/issues/103763
//
// we are fine with this, worst case is just an extra var update
#[allow(clippy::vtable_address_comparisons)]
fn eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(None, None) => true,
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
impl StyleFn {
/// Default function, produces an empty style.
pub fn nil() -> Self {
Self(None)
}
/// If this function represents no style.
pub fn is_nil(&self) -> bool {
self.0.is_none()
}
/// New style function, the `func` closure is called for each styleable widget, before the widget is inited.
pub fn new(func: impl Fn(&StyleArgs) -> StyleBuilder + Send + Sync + 'static) -> Self {
Self(Some(Arc::new(move |a| {
let style = func(a);
if style.is_empty() {
None
} else {
Some(style)
}
})))
}
/// New style function that returns clones of `style`.
///
/// Note that if the `style` contains properties with node values the nodes will be moved to
/// the last usage of the style, as nodes can't be cloned.
///
/// Also note that the `style` will stay in memory for the lifetime of the `StyleFn`.
pub fn singleton(style: StyleBuilder) -> Self {
Self::new(move |_| style.clone())
}
/// Call the function to create a style for the styleable widget in the context.
///
/// Returns `None` if [`is_nil`] or empty, otherwise returns the style.
///
/// Note that you can call the style function directly:
///
/// ```
/// use zero_ui::widgets::style::{StyleFn, StyleArgs};
///
/// fn foo(func: &StyleFn) {
/// let a = func.call(&StyleArgs {});
/// let b = func(&StyleArgs {});
/// }
/// ```
///
/// In the example above `a` and `b` are both calls to the style function.
///
/// [`is_nil`]: Self::is_nil
pub fn call(&self, args: &StyleArgs) -> Option<StyleBuilder> {
self.0.as_ref()?(args)
}
/// New style function that instantiates `self` and `other` and then [`extend`] `self` with `other`.
///
/// [`extend`]: StyleBuilder::extend
pub fn with_extend(self, other: StyleFn) -> StyleFn {
if self.is_nil() {
other
} else if other.is_nil() {
self
} else {
StyleFn::new(move |args| match (self(args), other(args)) {
(Some(mut a), Some(b)) => {
a.extend(b);
a
}
(Some(r), None) | (None, Some(r)) => r,
_ => StyleBuilder::default(),
})
}
}
}
impl fmt::Debug for StyleFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "StyleFn(_)")
}
}
impl ops::Deref for StyleFn {
type Target = dyn Fn(&StyleArgs) -> Option<StyleBuilder>;
fn deref(&self) -> &Self::Target {
if let Some(func) = &self.0 {
&**func
} else {
&nil_func
}
}
}
fn nil_func(_: &StyleArgs) -> Option<StyleBuilder> {
None
}
/// <span data-del-macro-root></span> Declares a style function closure.
///
/// The output type is a [`StyleFn`], the closure is [`clmv!`].
///
/// [`clmv!`]: crate::core::handler::clmv
#[macro_export]
macro_rules! style_fn {
($($tt:tt)+) => {
$crate::style::StyleFn::new($crate::core::handler::clmv! {
$($tt)+
})
}
}

View File

@ -0,0 +1,31 @@
[package]
name = "zero-ui-wgt-text"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-wgt-view = { path = "../zero-ui-wgt-view" }
zero-ui-wgt-access = { path = "../zero-ui-wgt-access" }
zero-ui-wgt-data = { path = "../zero-ui-wgt-data" }
zero-ui-wgt-size_offset = { path = "../zero-ui-wgt-size_offset" }
zero-ui-ext-window = { path = "../zero-ui-ext-window" }
zero-ui-ext-font = { path = "../zero-ui-ext-font" }
zero-ui-ext-input = { path = "../zero-ui-ext-input" }
zero-ui-ext-undo = { path = "../zero-ui-ext-undo" }
zero-ui-ext-clipboard = { path = "../zero-ui-ext-clipboard" }
zero-ui-ext-l10n = { path = "../zero-ui-ext-l10n" }
zero-ui-app = { path = "../zero-ui-app" }
zero-ui-view-api = { path = "../zero-ui-view-api" }
zero-ui-color = { path = "../zero-ui-color" }
zero-ui-layout = { path = "../zero-ui-layout" }
atomic = "0.6"
tracing = "0.1"
serde = "1"
parking_lot = "0.12"
bitflags = "2"
bytemuck = "1"
euclid = "0.22"

File diff suppressed because it is too large Load Diff

247
zero-ui-wgt-text/src/lib.rs Normal file
View File

@ -0,0 +1,247 @@
//! Text widgets and properties.
use zero_ui_wgt::prelude::*;
#[macro_use]
extern crate bitflags;
pub mod commands;
pub mod nodes;
mod text_properties;
pub use text_properties::*;
/// A configured text run.
///
/// # Examples
///
/// ```
/// use zero_ui::prelude::*;
///
/// let hello_txt = Text! {
/// font_family = "Arial";
/// font_size = 18;
/// txt = "Hello!";
/// };
/// ```
/// # Shorthand
///
/// The `Text!` macro provides shorthand syntax that matches the [`formatx!`] input, but outputs a text widget:
///
/// ```
/// # use zero_ui::prelude::*;
/// let txt = Text!("Hello!");
///
/// let name = "World";
/// let fmt = Text!("Hello {}!", name);
///
/// let expr = Text!({
/// let mut s = String::new();
/// s.push('a');
/// s
/// });
/// ```
///
/// The code abode is equivalent to:
///
/// ```
/// # use zero_ui::prelude::*;
/// let txt = Text! {
/// txt = zero_ui::core::text::formatx!("Hello!");
/// };
///
/// let name = "World";
/// let fmt = Text! {
/// txt = zero_ui::core::text::formatx!("Hello {}!", name);
/// };
///
/// let expr = Text! {
/// txt = {
/// let mut s = String::new();
/// s.push('a');
/// s
/// };
/// };
/// ```
///
/// [`formatx!`]: crate::core::text::formatx!
#[widget($crate::Text {
($txt:literal) => {
txt = $crate::core::text::formatx!($txt);
};
($txt:expr) => {
txt = $txt;
};
($txt:tt, $($format:tt)*) => {
txt = $crate::core::text::formatx!($txt, $($format)*);
};
})]
#[rustfmt::skip]
pub struct Text(
FontMix<
TextFillMix<
TextAlignMix<
TextWrapMix<
TextDecorationMix<
TextSpacingMix<
TextTransformMix<
LangMix<
FontFeaturesMix<
TextEditMix<
WidgetBase
>>>>>>>>>>
);
impl Text {
/// Context variables used by properties in text.
pub fn context_vars_set(set: &mut ContextValueSet) {
FontMix::<()>::context_vars_set(set);
TextFillMix::<()>::context_vars_set(set);
TextAlignMix::<()>::context_vars_set(set);
TextWrapMix::<()>::context_vars_set(set);
TextDecorationMix::<()>::context_vars_set(set);
TextSpacingMix::<()>::context_vars_set(set);
TextTransformMix::<()>::context_vars_set(set);
FontFeaturesMix::<()>::context_vars_set(set);
TextEditMix::<()>::context_vars_set(set);
LangMix::<()>::context_vars_set(set);
}
}
/// The text string.
///
/// Set to an empty string (`""`) by default.
#[property(CHILD, capture, default(""), widget_impl(Text))]
pub fn txt(txt: impl IntoVar<Txt>) {}
/// Value that is parsed from the text and displayed as the text.
///
/// This is an alternative to [`txt`] that converts to and from `T` if it can be formatted to display text and can parse, with
/// parse error that can display.
///
/// If the parse operation fails the value variable is not updated and the error display text is set in [`DATA.invalidate`], you
/// can use [`has_data_error`] and [`get_data_error_txt`] to display the error.
///
/// See also [`txt_parse_live`] for ways to control when the parse attempt happens.
///
/// [`txt`]: fn@txt
/// [`txt_parse_live`]: fn@txt_parse_live
/// [`DATA.invalidate`]: crate::properties::data_context::DATA::invalidate
/// [`has_data_error`]: fn@crate::properties::data_context::has_data_error
/// [`get_data_error_txt`]: fn@crate::properties::data_context::get_data_error_txt
#[property(CHILD, widget_impl(Text))]
pub fn txt_parse<T>(child: impl UiNode, value: impl IntoVar<T>) -> impl UiNode
where
T: TxtParseValue,
{
nodes::parse_text(child, value)
}
/// A type that can be a var value, parse and display.
///
/// This trait is used by [`txt_parse`]. It is implemented for all types that are
/// `VarValue + FromStr + Display where FromStr::Err: Display`.
///
/// [`txt_parse`]: fn@txt_parse
pub trait TxtParseValue: VarValue {
/// Try parse `Self` from `txt`, formats the error for display.
///
/// Note that the widget context is not available here as this method is called in the app context.
fn from_txt(txt: &Txt) -> Result<Self, Txt>;
/// Display the value, the returned text can be parsed back to an equal value.
///
/// Note that the widget context is not available here as this method is called in the app context.
fn to_txt(&self) -> Txt;
}
impl<T> TxtParseValue for T
where
T: VarValue + std::str::FromStr + std::fmt::Display,
<Self as std::str::FromStr>::Err: std::fmt::Display,
{
fn from_txt(txt: &Txt) -> Result<Self, Txt> {
T::from_str(txt).map_err(|e| e.to_text())
}
fn to_txt(&self) -> Txt {
self.to_text()
}
}
impl Text {
fn widget_intrinsic(&mut self) {
self.widget_builder().push_build_action(|wgt| {
let child = nodes::render_text();
let child = nodes::render_caret(child);
let child = nodes::touch_carets(child);
let child = nodes::render_overlines(child);
let child = nodes::render_strikethroughs(child);
let child = nodes::render_underlines(child);
let child = nodes::render_ime_preview_underlines(child);
let child = nodes::render_selection(child);
wgt.set_child(child.boxed());
wgt.push_intrinsic(NestGroup::CHILD_LAYOUT + 100, "layout_text", |child| {
let child = nodes::selection_toolbar_node(child);
nodes::layout_text(child)
});
let text = if wgt.property(property_id!(Self::txt_parse)).is_some() {
wgt.capture_var(property_id!(Self::txt))
.unwrap_or_else(|| var(Txt::from_str("")).boxed())
} else {
wgt.capture_var_or_default(property_id!(Self::txt))
};
wgt.push_intrinsic(NestGroup::EVENT, "resolve_text", |child| nodes::resolve_text(child, text));
});
}
}
///<span data-del-macro-root></span> A simple text run with **bold** font weight.
///
/// The input syntax is the same as the shorthand [`Text!`].
///
/// # Configure
///
/// Apart from the font weight this widget can be configured with contextual properties like [`Text!`].
///
/// [`Text!`]: struct@Text
#[macro_export]
macro_rules! Strong {
($txt:expr) => {
$crate::widgets::Text! {
txt = $txt;
font_weight = $crate::core::text::FontWeight::BOLD;
}
};
($txt:tt, $($format:tt)*) => {
$crate::widgets::Text! {
txt = $crate::core::text::formatx!($txt, $($format)*);
font_weight = $crate::core::text::FontWeight::BOLD;
}
};
}
///<span data-del-macro-root></span> A simple text run with *italic* font style.
///
/// The input syntax is the same as the shorthand [`Text!`].
///
/// # Configure
///
/// Apart from the font style this widget can be configured with contextual properties like [`Text!`].
///
/// [`Text!`]: struct@Text
#[macro_export]
macro_rules! Em {
($txt:expr) => {
$crate::widgets::Text! {
txt = $txt;
font_style = FontStyle::Italic;
}
};
($txt:tt, $($format:tt)*) => {
$crate::widgets::Text! {
txt = $crate::core::text::formatx!($txt, $($format)*);
font_style = FontStyle::Italic;
}
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
[package]
name = "zero-ui-wgt-transform"
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-wgt = { path = "../zero-ui-wgt" }
euclid = "0.22"

View File

@ -0,0 +1,343 @@
//! Transform properties, [`scale`](fn@scale), [`rotate`](fn@rotate), [`transform`](fn@transform) and more.
use zero_ui_wgt::prelude::*;
/// Custom transform.
///
/// See [`Transform`] for how to initialize a custom transform. The [`transform_origin`] is applied using the widget's inner size
/// for relative values.
///
/// [`transform_origin`]: fn@transform_origin
#[property(LAYOUT, default(Transform::identity()))]
pub fn transform(child: impl UiNode, transform: impl IntoVar<Transform>) -> impl UiNode {
let binding_key = FrameValueKey::new_unique();
let transform = transform.into_var();
let mut render_transform = PxTransform::identity();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&transform);
}
UiNodeOp::Layout { wl, final_size } => {
let size = child.layout(wl);
let transform = transform.layout();
let default_origin = PxPoint::new(size.width / 2.0, size.height / 2.0);
let origin = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(size), || {
TRANSFORM_ORIGIN_VAR.layout_dft(default_origin)
});
let x = origin.x.0 as f32;
let y = origin.y.0 as f32;
let transform = PxTransform::translation(-x, -y).then(&transform).then_translate(euclid::vec2(x, y));
if transform != render_transform {
render_transform = transform;
WIDGET.render_update();
}
*final_size = size;
}
UiNodeOp::Render { frame } => {
if frame.is_outer() {
frame.push_inner_transform(&render_transform, |frame| child.render(frame));
} else {
frame.push_reference_frame(
binding_key.into(),
binding_key.bind_var_mapped(&transform, render_transform),
false,
false,
|frame| child.render(frame),
);
}
}
UiNodeOp::RenderUpdate { update } => {
if update.is_outer() {
update.with_inner_transform(&render_transform, |update| child.render_update(update));
} else {
update.with_transform_opt(binding_key.update_var_mapped(&transform, render_transform), false, |update| {
child.render_update(update)
})
}
}
_ => {}
})
}
/// Rotate transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_rotate(angle)`](Transform::new_rotate) using variable mapping.
///
/// The rotation is done *around* the [`transform_origin`] in 2D.
///
/// [`transform`]: fn@transform
/// [`transform_origin`]: fn@transform_origin
#[property(LAYOUT, default(0.rad()))]
pub fn rotate(child: impl UiNode, angle: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, angle.into_var().map(|&a| Transform::new_rotate(a)))
}
/// Rotate transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_rotate_x(angle)`](Transform::new_rotate_x) using variable mapping.
///
/// The rotation is done *around* the ***x*** axis that passes trough the [`transform_origin`] in 3D.
///
/// [`transform`]: fn@transform
/// [`transform_origin`]: fn@transform_origin
#[property(LAYOUT, default(0.rad()))]
pub fn rotate_x(child: impl UiNode, angle: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, angle.into_var().map(|&a| Transform::new_rotate_x(a)))
}
/// Rotate transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_rotate_y(angle)`](Transform::new_rotate_y) using variable mapping.
///
/// The rotation is done *around* the ***y*** axis that passes trough the [`transform_origin`] in 3D.
///
/// [`transform`]: fn@transform
/// [`transform_origin`]: fn@transform_origin
#[property(LAYOUT, default(0.rad()))]
pub fn rotate_y(child: impl UiNode, angle: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, angle.into_var().map(|&a| Transform::new_rotate_y(a)))
}
/// Same as [`rotate`].
///
/// [`rotate`]: fn@rotate
#[property(LAYOUT, default(0.rad()))]
pub fn rotate_z(child: impl UiNode, angle: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, angle.into_var().map(|&a| Transform::new_rotate_z(a)))
}
/// Scale transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_scale(s)`](Transform::new_scale) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(1.0))]
pub fn scale(child: impl UiNode, s: impl IntoVar<Factor>) -> impl UiNode {
transform(child, s.into_var().map(|&x| Transform::new_scale(x)))
}
/// Scale X and Y transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_scale_xy(x, y)`](Transform::new_scale) using variable merging.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(1.0, 1.0))]
pub fn scale_xy(child: impl UiNode, x: impl IntoVar<Factor>, y: impl IntoVar<Factor>) -> impl UiNode {
transform(
child,
merge_var!(x.into_var(), y.into_var(), |&x, &y| Transform::new_scale_xy(x, y)),
)
}
/// Scale X transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_scale_x(x)`](Transform::new_scale_x) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(1.0))]
pub fn scale_x(child: impl UiNode, x: impl IntoVar<Factor>) -> impl UiNode {
transform(child, x.into_var().map(|&x| Transform::new_scale_x(x)))
}
/// Scale Y transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_scale_y(y)`](Transform::new_scale_y) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(1.0))]
pub fn scale_y(child: impl UiNode, y: impl IntoVar<Factor>) -> impl UiNode {
transform(child, y.into_var().map(|&y| Transform::new_scale_y(y)))
}
/// Skew transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_skew(x, y)`](Transform::new_skew) using variable merging.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(0.rad(), 0.rad()))]
pub fn skew(child: impl UiNode, x: impl IntoVar<AngleRadian>, y: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, merge_var!(x.into_var(), y.into_var(), |&x, &y| Transform::new_skew(x, y)))
}
/// Skew X transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_skew_x(x)`](Transform::new_skew_x) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(0.rad()))]
pub fn skew_x(child: impl UiNode, x: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, x.into_var().map(|&x| Transform::new_skew_x(x)))
}
/// Skew Y transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_skew_y(y)`](Transform::new_skew_y) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT)]
pub fn skew_y(child: impl UiNode, y: impl IntoVar<AngleRadian>) -> impl UiNode {
transform(child, y.into_var().map(|&y| Transform::new_skew_y(y)))
}
/// Translate transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_translate(x, y)`](Transform::new_translate) using variable merging.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(0, 0))]
pub fn translate(child: impl UiNode, x: impl IntoVar<Length>, y: impl IntoVar<Length>) -> impl UiNode {
transform(
child,
merge_var!(x.into_var(), y.into_var(), |x, y| Transform::new_translate(x.clone(), y.clone())),
)
}
/// Translate X transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_translate_x(x)`](Transform::new_translate_x) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(0))]
pub fn translate_x(child: impl UiNode, x: impl IntoVar<Length>) -> impl UiNode {
transform(child, x.into_var().map(|x| Transform::new_translate_x(x.clone())))
}
/// Translate Y transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_translate_y(y)`](Transform::new_translate_y) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(0))]
pub fn translate_y(child: impl UiNode, y: impl IntoVar<Length>) -> impl UiNode {
transform(child, y.into_var().map(|y| Transform::new_translate_y(y.clone())))
}
/// Translate Z transform.
///
/// This property is a shorthand way of setting [`transform`] to [`new_translate_z(z)`](Transform::new_translate_z) using variable mapping.
///
/// [`transform`]: fn@transform
#[property(LAYOUT, default(0))]
pub fn translate_z(child: impl UiNode, z: impl IntoVar<Length>) -> impl UiNode {
transform(child, z.into_var().map(|z| Transform::new_translate_z(z.clone())))
}
/// Point relative to the widget inner bounds around which the [`transform`] is applied.
///
/// This property sets the [`TRANSFORM_ORIGIN_VAR`] context variable.
///
/// [`transform`]: fn@transform
#[property(CONTEXT, default(TRANSFORM_ORIGIN_VAR))]
pub fn transform_origin(child: impl UiNode, origin: impl IntoVar<Point>) -> impl UiNode {
with_context_var(child, TRANSFORM_ORIGIN_VAR, origin)
}
///Distance from the Z plane (0) the viewer is, affects 3D transform on the widget's children.
///
/// [`Length::Default`] is an infinite distance, the lower the value the *closest* the viewer is and therefore
/// the 3D transforms are more noticeable. Distances less then `1.px()` are coerced to it.
#[property(LAYOUT-20, default(Length::Default))]
pub fn perspective(child: impl UiNode, distance: impl IntoVar<Length>) -> impl UiNode {
let distance = distance.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&distance);
}
UiNodeOp::Layout { wl, .. } => {
let d = distance.layout_dft_z(Px::MAX);
let d = LAYOUT.z_constraints().clamp(d).max(Px(1));
wl.set_perspective(d.0 as f32);
}
_ => {}
})
}
/// Vanishing point used 3D transforms in the widget's children.
///
/// Is the widget center by default.
///
/// [`transform`]: fn@transform
#[property(LAYOUT-20, default(Point::default()))]
pub fn perspective_origin(child: impl UiNode, origin: impl IntoVar<Point>) -> impl UiNode {
let origin = origin.into_var();
match_node(child, move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&origin);
}
UiNodeOp::Layout { wl, final_size } => {
let size = c.layout(wl);
let default_origin = PxPoint::new(size.width / 2.0, size.height / 2.0);
let origin = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(size), || origin.layout_dft(default_origin));
wl.set_perspective_origin(origin);
*final_size = size;
}
_ => {}
})
}
/// Defines how the widget and children are positioned in 3D space.
///
/// This sets the style for the widget and children layout transform, the [`transform`] and other properties derived from [`transform`].
/// It does not affect any other descendant, only the widget and immediate children.
///
/// [`transform`]: fn@transform
#[property(CONTEXT, default(TransformStyle::Flat))]
pub fn transform_style(child: impl UiNode, style: impl IntoVar<TransformStyle>) -> impl UiNode {
let style = style.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&style);
}
UiNodeOp::Layout { wl, .. } => {
wl.set_transform_style(style.get());
}
_ => {}
})
}
/// Sets if the widget is still visible when it is turned back towards the viewport due to rotations in X or Y axis in
/// the widget or in parent widgets.
///
/// Widget back face is visible by default, the back face is a mirror image of the front face, if `visible` is set
/// to `false` the widget is still layout and rendered, but it is not displayed on screen by the view-process if
/// the final global transform of the widget turns the backface towards the viewport.
///
/// This property affects any descendant widgets too, unless they also set `backface_visibility`.
#[property(CONTEXT, default(true))]
pub fn backface_visibility(child: impl UiNode, visible: impl IntoVar<bool>) -> impl UiNode {
let visible = visible.into_var();
match_node(child, move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&visible);
}
UiNodeOp::Render { frame } => {
frame.with_backface_visibility(visible.get(), |frame| c.render(frame));
}
_ => {}
})
}
context_var! {
/// Point relative to the widget inner bounds around which the [`transform`] is applied.
///
/// Default origin is [`Point::center()`].
///
/// [`transform`]: fn@transform
pub static TRANSFORM_ORIGIN_VAR: Point = Point::center();
/// Vanishing point used by [`transform`] when it is 3D.
///
/// Default origin is [`Point::center()`].
///
/// [`transform`]: fn@transform
pub static PERSPECTIVE_ORIGIN_VAR: Point = Point::center();
}

View File

@ -0,0 +1,10 @@
[package]
name = "zero-ui-wgt-undo"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-ext-undo = { path = "../zero-ui-ext-undo" }

156
zero-ui-wgt-undo/src/lib.rs Normal file
View File

@ -0,0 +1,156 @@
use std::time::{Duration, Instant};
use zero_ui_ext_undo::*;
use zero_ui_wgt::prelude::*;
/// Sets if the widget is an undo scope.
///
/// If `true` the widget will handle [`UNDO_CMD`] and [`REDO_CMD`] for all undo actions
/// that happen inside it.
#[property(CONTEXT - 10, default(false))]
pub fn undo_scope(child: impl UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
let mut scope = WidgetUndoScope::new();
let mut undo_cmd = CommandHandle::dummy();
let mut redo_cmd = CommandHandle::dummy();
let mut clear_cmd = CommandHandle::dummy();
let is_scope = is_scope.into_var();
match_node(child, move |c, mut op| {
match &mut op {
UiNodeOp::Init => {
WIDGET.sub_var(&is_scope);
if !is_scope.get() {
return; // default handling without scope context.
}
scope.init();
let id = WIDGET.id();
undo_cmd = UNDO_CMD.scoped(id).subscribe(false);
redo_cmd = REDO_CMD.scoped(id).subscribe(false);
clear_cmd = CLEAR_HISTORY_CMD.scoped(id).subscribe(false);
}
UiNodeOp::Deinit => {
if !is_scope.get() {
return;
}
UNDO.with_scope(&mut scope, || c.deinit());
scope.deinit();
undo_cmd = CommandHandle::dummy();
redo_cmd = CommandHandle::dummy();
return;
}
UiNodeOp::Info { info } => {
if !is_scope.get() {
return;
}
scope.info(info);
}
UiNodeOp::Event { update } => {
if !is_scope.get() {
return;
}
let id = WIDGET.id();
if let Some(args) = UNDO_CMD.scoped(id).on_unhandled(update) {
args.propagation().stop();
UNDO.with_scope(&mut scope, || {
if let Some(&n) = args.param::<u32>() {
UNDO.undo_select(n);
} else if let Some(&i) = args.param::<Duration>() {
UNDO.undo_select(i);
} else if let Some(&t) = args.param::<Instant>() {
UNDO.undo_select(t);
} else {
UNDO.undo();
}
});
} else if let Some(args) = REDO_CMD.scoped(id).on_unhandled(update) {
args.propagation().stop();
UNDO.with_scope(&mut scope, || {
if let Some(&n) = args.param::<u32>() {
UNDO.redo_select(n);
} else if let Some(&i) = args.param::<Duration>() {
UNDO.redo_select(i);
} else if let Some(&t) = args.param::<Instant>() {
UNDO.redo_select(t);
} else {
UNDO.redo();
}
});
} else if let Some(args) = CLEAR_HISTORY_CMD.scoped(id).on_unhandled(update) {
args.propagation().stop();
UNDO.with_scope(&mut scope, || {
UNDO.clear();
});
}
}
UiNodeOp::Update { .. } => {
if let Some(is_scope) = is_scope.get_new() {
WIDGET.info();
if is_scope {
if !scope.is_inited() {
scope.init();
let id = WIDGET.id();
undo_cmd = UNDO_CMD.scoped(id).subscribe(false);
redo_cmd = REDO_CMD.scoped(id).subscribe(false);
}
} else if scope.is_inited() {
scope.deinit();
undo_cmd = CommandHandle::dummy();
redo_cmd = CommandHandle::dummy();
}
}
if !is_scope.get() {
return;
}
}
_ => {
if !is_scope.get() {
return;
}
}
}
UNDO.with_scope(&mut scope, || c.op(op));
let can_undo = scope.can_undo();
let can_redo = scope.can_redo();
undo_cmd.set_enabled(can_undo);
redo_cmd.set_enabled(can_redo);
clear_cmd.set_enabled(can_undo || can_redo);
})
}
/// Enable or disable undo inside the widget.
#[property(CONTEXT, default(true))]
pub fn undo_enabled(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
match_node(child, move |c, op| {
if !enabled.get() {
UNDO.with_disabled(|| c.op(op))
}
})
}
/// Sets the maximum length for undo/redo stacks in the widget and descendants.
///
/// This property sets the [`UNDO_LIMIT_VAR`].
#[property(CONTEXT - 11, default(UNDO_LIMIT_VAR))]
pub fn undo_limit(child: impl UiNode, max: impl IntoVar<u32>) -> impl UiNode {
with_context_var(child, UNDO_LIMIT_VAR, max)
}
/// Sets the time interval that undo and redo cover each call for undo handlers in the widget and descendants.
///
/// When undo is requested inside the context all actions after the latest that are within `interval` of the
/// previous are undone.
///
/// This property sets the [`UNDO_INTERVAL_VAR`].
#[property(CONTEXT - 11, default(UNDO_INTERVAL_VAR))]
pub fn undo_interval(child: impl UiNode, interval: impl IntoVar<Duration>) -> impl UiNode {
with_context_var(child, UNDO_INTERVAL_VAR, interval)
}

View File

@ -0,0 +1,18 @@
[package]
name = "zero-ui-wgt-view"
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-wgt = { path = "../zero-ui-wgt" }
zero-ui-var = { path = "../zero-ui-var" }
zero-ui-wgt-container = { path = "../zero-ui-wgt-container" }
pretty-type-name = "1"
parking_lot = "0.12"
tracing = "0.1"
[dev-dependencies]
zero-ui-app = { path = "../zero-ui-app" }

598
zero-ui-wgt-view/src/lib.rs Normal file
View File

@ -0,0 +1,598 @@
//! Dynamic widget instantiation from data.
use parking_lot::Mutex;
use pretty_type_name::*;
use std::{any::TypeId, fmt, ops, sync::Arc};
use zero_ui_wgt::prelude::*;
mod vec;
pub use vec::{ObservableVec, VecChange};
#[doc(hidden)]
pub use zero_ui_wgt::prelude::clmv as __clmv;
type BoxedWgtFn<D> = Box<dyn Fn(D) -> BoxedUiNode + Send + Sync>;
/// Boxed shared closure that generates an widget for a given data.
///
/// # Examples
///
/// Define the content that is shown when an image fails to load:
///
/// ```
/// # use zero_ui::prelude::*;
/// # let _ =
/// Image! {
/// source = "not_found.png";
/// img_error_fn = WidgetFn::new(|e: image::ImgErrorArgs| Text! {
/// txt = e.error.clone();
/// font_color = colors::RED;
/// });
/// }
/// # ;
/// ```
///
/// You can also use the [`wgt_fn!`] macro, it has the advantage of being clone move.
///
/// See [`presenter`] for a way to quickly use the widget function in the UI.
pub struct WidgetFn<D: ?Sized>(Option<Arc<BoxedWgtFn<D>>>);
impl<D> Clone for WidgetFn<D> {
fn clone(&self) -> Self {
WidgetFn(self.0.clone())
}
}
impl<D> fmt::Debug for WidgetFn<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "WidgetFn<{}>", pretty_type_name::<D>())
}
}
impl<D> PartialEq for WidgetFn<D> {
fn eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(None, None) => true,
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
impl<D> Default for WidgetFn<D> {
/// `nil`.
fn default() -> Self {
Self::nil()
}
}
impl<D> WidgetFn<D> {
/// New from a closure that generates a node from data.
pub fn new<U: UiNode>(func: impl Fn(D) -> U + Send + Sync + 'static) -> Self {
WidgetFn(Some(Arc::new(Box::new(move |data| func(data).boxed()))))
}
/// Function that always produces the [`NilUiNode`].
///
/// No heap allocation happens to create this value.
///
/// # Examples
///
/// ```
/// # use zero_ui::{core::var::context_var, widgets::WidgetFn};
/// # pub struct Foo;
/// context_var! {
/// /// Widget function for `Foo` items.
/// pub static FOO_FN_VAR: WidgetFn<Foo> = WidgetFn::nil();
/// }
/// ```
pub const fn nil() -> Self {
WidgetFn(None)
}
/// If this is the [`nil`] function.
///
/// [`nil`]: WidgetFn::nil
pub fn is_nil(&self) -> bool {
self.0.is_none()
}
/// Calls the function with `data` argument.
///
/// Note that you can call the widget function directly where `D: 'static`:
///
/// ```
/// use zero_ui::prelude::*;
///
/// fn foo(func: &WidgetFn<bool>) {
/// let a = func.call(true);
/// let b = func(true);
/// }
/// ```
///
/// In the example above `a` and `b` are both calls to the widget function.
pub fn call(&self, data: D) -> BoxedUiNode {
if let Some(g) = &self.0 {
g(data)
} else {
NilUiNode.boxed()
}
}
/// New widget function that returns the same `widget` for every call.
///
/// The `widget` is wrapped in an [`ArcNode`] and every function call returns an [`ArcNode::take_on_init`] node.
/// Note that `take_on_init` is not always the `widget` on init as it needs to wait for it to deinit first if
/// it is already in use, this could have an effect if the the widget function caller always expects a full widget.
pub fn singleton(widget: impl UiNode) -> Self {
let widget = ArcNode::new(widget);
Self::new(move |_| widget.take_on_init())
}
}
impl<D: 'static> ops::Deref for WidgetFn<D> {
type Target = dyn Fn(D) -> BoxedUiNode;
fn deref(&self) -> &Self::Target {
match self.0.as_ref() {
Some(f) => &**f,
None => &nil_call::<D>,
}
}
}
fn nil_call<D>(_: D) -> BoxedUiNode {
NilUiNode.boxed()
}
/// <span data-del-macro-root></span> Declares a widget function closure.
///
/// The output type is a [`WidgetFn`], the closure is [`clmv!`].
///
/// # Examples
///
/// Define the content that is shown when an image fails to load, capturing another variable too.
///
/// ```
/// # use zero_ui::prelude::*;
/// let img_error_vis = var(Visibility::Visible);
/// # let _ =
/// Image! {
/// source = "not_found.png";
/// img_error_fn = wgt_fn!(img_error_vis, |e: image::ImgErrorArgs| Text! {
/// txt = e.error.clone();
/// font_color = colors::RED;
/// visibility = img_error_vis.clone();
/// });
/// }
/// # ;
/// ```
///
/// [`WidgetFn`]: crate::widgets::WidgetFn
/// [`clmv!`]: crate::core::handler::clmv
#[macro_export]
macro_rules! wgt_fn {
($($tt:tt)+) => {
$crate::WidgetFn::new($crate::__clmv! {
$($tt)+
})
};
() => {
$crate::widgets::WidgetFn::nil()
};
}
/// Node that presents `data` using `update`.
///
/// The node's child is always the result of `update` for the `data` value, it is reinited every time
/// either `data` or `update` updates.
///
/// See also [`presenter_opt`] for a presenter that is nil with the data is `None` and [`View!`] for
/// avoiding a info tree rebuild for every data update.
///
/// Note that this node is not a full widget, it can be used as part of an widget without adding to the info tree.
///
/// [`View!`]: struct@View
pub fn presenter<D: VarValue>(data: impl IntoVar<D>, update: impl IntoVar<WidgetFn<D>>) -> impl UiNode {
let data = data.into_var();
let update = update.into_var();
match_node(NilUiNode.boxed(), move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&data).sub_var(&update);
*c.child() = update.get()(data.get());
}
UiNodeOp::Deinit => {
c.deinit();
*c.child() = NilUiNode.boxed();
}
UiNodeOp::Update { .. } => {
if data.is_new() || update.is_new() {
c.child().deinit();
*c.child() = update.get()(data.get());
c.child().init();
c.delegated();
WIDGET.update_info().layout().render();
}
}
_ => {}
})
}
/// Node that presents `data` using `update` if data is available, otherwise presents nil.
///
/// This behaves like [`presenter`], but `update` is not called if `data` is `None`.
///
/// Note that this node is not a full widget, it can be used as part of an widget without adding to the info tree.
pub fn presenter_opt<D: VarValue>(data: impl IntoVar<Option<D>>, update: impl IntoVar<WidgetFn<D>>) -> impl UiNode {
let data = data.into_var();
let update = update.into_var();
match_node(NilUiNode.boxed(), move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&data).sub_var(&update);
if let Some(data) = data.get() {
*c.child() = update.get()(data);
}
}
UiNodeOp::Deinit => {
c.deinit();
*c.child() = NilUiNode.boxed();
}
UiNodeOp::Update { .. } => {
if data.is_new() || update.is_new() {
if let Some(data) = data.get() {
c.child().deinit();
*c.child() = update.get()(data);
c.child().init();
c.delegated();
WIDGET.update_info().layout().render();
} else if c.child().actual_type_id() != TypeId::of::<NilUiNode>() {
c.child().deinit();
*c.child() = NilUiNode.boxed();
c.delegated();
WIDGET.update_info().layout().render();
}
}
}
_ => {}
})
}
/// Arguments for the [`View!`] widget.
///
/// [`View!`]: struct@View
#[derive(Clone)]
pub struct ViewArgs<D: VarValue> {
data: BoxedVar<D>,
replace: Arc<Mutex<Option<BoxedUiNode>>>,
is_nil: bool,
}
impl<D: VarValue> ViewArgs<D> {
/// Reference the data variable.
///
/// Can be cloned and used in the [`set_view`] to avoid rebuilding the info tree for every update.
///
/// [`set_view`]: Self::set_view
pub fn data(&self) -> &BoxedVar<D> {
&self.data
}
/// If the current child is [`NilUiNode`];
pub fn is_nil(&self) -> bool {
self.is_nil
}
/// Get the current data value if [`is_nil`] or [`data`] is new.
///
/// [`is_nil`]: Self::is_nil
/// [`data`]: Self::data
pub fn get_new(&self) -> Option<D> {
if self.is_nil {
Some(self.data.get())
} else {
self.data.get_new()
}
}
/// Replace the child node.
///
/// If set the current child node will be deinited and dropped.
pub fn set_view(&self, new_child: impl UiNode) {
*self.replace.lock() = Some(new_child.boxed());
}
/// Set the view to [`NilUiNode`].
pub fn unset_view(&self) {
self.set_view(NilUiNode)
}
}
/// Dynamically presents a data variable.
///
/// The `update` widget handler is used to generate the view UI from the `data`, it is called on init and
/// every time `data` or `update` are new. The view is set by calling [`ViewArgs::set_view`] in the widget function
/// args, note that the data variable is available in [`ViewArgs::data`], a good view will bind to the variable
/// to support some changes, only replacing the UI for major changes.
///
/// Note that this node is not a full widget, it can be used as part of an widget without adding to the info tree.
///
/// # Examples
///
/// View using the shorthand syntax:
///
/// ```
/// use zero_ui::prelude::*;
///
/// fn countdown(n: impl IntoVar<usize>) -> impl UiNode {
/// View!(::<usize>, n, hn!(|a: &ViewArgs<usize>| {
/// // we generate a new view on the first call or when the data has changed to zero.
/// if a.is_nil() || a.data().get_new() == Some(0) {
/// a.set_view(if a.data().get() > 0 {
/// // countdown view
/// Text! {
/// font_size = 28;
/// // bind data, same view will be used for all n > 0 values.
/// txt = a.data().map_to_text();
/// }
/// } else {
/// // finished view
/// Text! {
/// font_color = rgb(0, 128, 0);
/// font_size = 18;
/// txt = "Congratulations!";
/// }
/// });
/// }
/// }))
/// }
/// ```
///
/// You can also use the normal widget syntax and set the `view` property.
///
/// ```
/// use zero_ui::prelude::*;
///
/// fn countdown(n: impl IntoVar<usize>) -> impl UiNode {
/// View! {
/// view::<usize> = {
/// data: n,
/// update: hn!(|a: &ViewArgs<usize>| { }),
/// };
/// background_color = colors::GRAY;
/// }
/// }
/// ```
#[widget($crate::View {
(::<$T:ty>, $data:expr, $update:expr $(,)?) => {
view::<$T> = {
data: $data,
update: $update,
};
}
})]
pub struct View(WidgetBase);
impl View {
widget_impl! {
/// Spacing around content, inside the border.
pub zero_ui_wgt_container::padding(padding: impl IntoVar<SideOffsets>);
/// Content alignment.
pub zero_ui_wgt_container::child_align(align: impl IntoVar<Align>);
/// Content overflow clipping.
pub zero_ui_wgt::clip_to_bounds(clip: impl IntoVar<bool>);
}
}
/// The view generator.
///
/// See [`View!`] for more details.
///
/// [`View!`]: struct@View
#[property(CHILD, widget_impl(View))]
pub fn view<D: VarValue>(child: impl UiNode, data: impl IntoVar<D>, update: impl WidgetHandler<ViewArgs<D>>) -> impl UiNode {
let data = data.into_var().boxed();
let mut update = update.cfg_boxed();
let replace = Arc::new(Mutex::new(None));
match_node(child.boxed(), move |c, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&data);
update.event(&ViewArgs {
data: data.clone(),
replace: replace.clone(),
is_nil: true,
});
if let Some(child) = replace.lock().take() {
*c.child() = child;
}
}
UiNodeOp::Deinit => {
c.deinit();
*c.child() = NilUiNode.boxed();
}
UiNodeOp::Update { .. } => {
if data.is_new() {
update.event(&ViewArgs {
data: data.clone(),
replace: replace.clone(),
is_nil: c.child().actual_type_id() == TypeId::of::<NilUiNode>(),
});
}
update.update();
if let Some(child) = replace.lock().take() {
// skip update if nil -> nil, otherwise updates
if c.child().actual_type_id() != TypeId::of::<NilUiNode>() || child.actual_type_id() != TypeId::of::<NilUiNode>() {
c.child().deinit();
*c.child() = child;
c.child().init();
c.delegated();
WIDGET.update_info().layout().render();
}
}
}
_ => {}
})
}
/// Node that presents `list` using `element_fn` for each new element.
///
/// The node's children is always the result of `element_fn` called for each element in the `list`, removed
/// elements are deinited, inserted elements get a call to `element_fn` and are inserted in the same position
/// on the list.
pub fn list_presenter<D: VarValue>(list: impl IntoVar<ObservableVec<D>>, element_fn: impl IntoVar<WidgetFn<D>>) -> impl UiNodeList {
ListPresenter {
list: list.into_var(),
element_fn: element_fn.into_var(),
view: vec![],
_e: std::marker::PhantomData,
}
}
struct ListPresenter<D: VarValue, L: Var<ObservableVec<D>>, E: Var<WidgetFn<D>>> {
list: L,
element_fn: E,
view: Vec<BoxedUiNode>,
_e: std::marker::PhantomData<D>,
}
impl<D, L, E> UiNodeList for ListPresenter<D, L, E>
where
D: VarValue,
L: Var<ObservableVec<D>>,
E: Var<WidgetFn<D>>,
{
fn with_node<R, F>(&mut self, index: usize, f: F) -> R
where
F: FnOnce(&mut BoxedUiNode) -> R,
{
self.view.with_node(index, f)
}
fn for_each<F>(&mut self, f: F)
where
F: FnMut(usize, &mut BoxedUiNode),
{
self.view.for_each(f)
}
fn par_each<F>(&mut self, f: F)
where
F: Fn(usize, &mut BoxedUiNode) + Send + Sync,
{
self.view.par_each(f)
}
fn par_fold_reduce<T, I, F, R>(&mut self, identity: I, fold: F, reduce: R) -> T
where
T: Send + 'static,
I: Fn() -> T + Send + Sync,
F: Fn(T, usize, &mut BoxedUiNode) -> T + Send + Sync,
R: Fn(T, T) -> T + Send + Sync,
{
self.view.par_fold_reduce(identity, fold, reduce)
}
fn len(&self) -> usize {
self.view.len()
}
fn boxed(self) -> BoxedUiNodeList {
Box::new(self)
}
fn drain_into(&mut self, vec: &mut Vec<BoxedUiNode>) {
self.view.drain_into(vec);
tracing::warn!("drained `list_presenter`, now out of sync with data");
}
fn init_all(&mut self) {
debug_assert!(self.view.is_empty());
self.view.clear();
WIDGET.sub_var(&self.list).sub_var(&self.element_fn);
let e_fn = self.element_fn.get();
self.list.with(|l| {
for el in l.iter() {
let child = e_fn(el.clone());
self.view.push(child);
}
});
self.view.init_all();
}
fn deinit_all(&mut self) {
self.view.deinit_all();
self.view.clear();
}
fn update_all(&mut self, updates: &WidgetUpdates, observer: &mut dyn UiNodeListObserver) {
let mut need_reset = self.element_fn.is_new();
let is_new = self
.list
.with_new(|l| {
need_reset |= l.changes().is_empty() || l.changes() == [VecChange::Clear];
if need_reset {
return;
}
// update before new items to avoid update before init.
self.view.update_all(updates, observer);
let e_fn = self.element_fn.get();
for change in l.changes() {
match change {
VecChange::Insert { index, count } => {
for i in *index..(*index + count) {
let mut el = e_fn(l[i].clone());
el.init();
self.view.insert(i, el);
observer.inserted(i);
}
}
VecChange::Remove { index, count } => {
let mut count = *count;
let index = *index;
while count > 0 {
count -= 1;
let mut el = self.view.remove(index);
el.deinit();
observer.removed(index);
}
}
VecChange::Move { from_index, to_index } => {
let el = self.view.remove(*from_index);
self.view.insert(*to_index, el);
observer.moved(*from_index, *to_index);
}
VecChange::Clear => unreachable!(),
}
}
})
.is_some();
if !need_reset && !is_new && self.list.with(|l| l.len() != self.view.len()) {
need_reset = true;
}
if need_reset {
self.view.deinit_all();
self.view.clear();
let e_fn = self.element_fn.get();
self.list.with(|l| {
for el in l.iter() {
let child = e_fn(el.clone());
self.view.push(child);
}
});
self.view.init_all();
} else if !is_new {
self.view.update_all(updates, observer);
}
}
}

521
zero-ui-wgt-view/src/vec.rs Normal file
View File

@ -0,0 +1,521 @@
use std::ops;
use zero_ui_var::{VarUpdateId, VARS};
use zero_ui_wgt::prelude::*;
/// Represents a [`Vec<T>`] that tracks changes when used inside a variable.
///
/// The changes made in the last update are available in [`ObservableVec::changes`].
///
/// This struct is designed to be a data source for [`list_presenter`], because it tracks
/// exact changes it enables the implementation of transition animations such as a new
/// element *expanding* into place, it also allows the retention of widget state for elements
/// that did not change.
///
/// Changes are logged using the [`VecChange`] enum, note that the enum only tracks indexes at the
/// moment the change happens, that means that you cannot get the removed items and [`VecChange::Insert`]
/// must be the last change in an update cycle. If any change is made that invalidates an `Insert` all
/// changes for the cycle are collapsed to [`VecChange::Clear`], to avoid this try removing or moving before insert.
///
/// [`list_presenter`]: crate::widgets::list_presenter
#[derive(Debug, Clone, PartialEq)]
pub struct ObservableVec<T: VarValue> {
list: Vec<T>,
changes: VecChanges,
}
impl<T: VarValue> Default for ObservableVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: VarValue> ops::Deref for ObservableVec<T> {
type Target = [T];
fn deref(&self) -> &Self::Target {
self.list.deref()
}
}
impl<T: VarValue> ObservableVec<T> {
/// New empty vec.
pub const fn new() -> Self {
Self {
list: vec![],
changes: VecChanges::new(),
}
}
/// New empty vec with pre-allocated capacity.
///
/// See [`Vec::with_capacity`].
pub fn with_capacity(capacity: usize) -> Self {
Self {
list: Vec::with_capacity(capacity),
changes: VecChanges::new(),
}
}
/// Reserves capacity for at least additional more elements.
///
/// See [`Vec::reserve`].
pub fn reserve(&mut self, additional: usize) {
self.list.reserve(additional);
}
/// Insert the `element` at the `index`.
///
/// See [`Vec::insert`].
pub fn insert(&mut self, index: usize, element: T) {
self.list.insert(index, element);
self.changes.inserted(index, 1);
}
/// Insert the `element` at the end of the vec.
///
/// See [`Vec::push`].
pub fn push(&mut self, element: T) {
self.insert(self.len(), element);
}
/// Moves all the elements of `other` into `self`, leaving `other` empty.
///
/// See [`Vec::append`].
pub fn append(&mut self, other: &mut Vec<T>) {
self.changes.inserted(self.list.len(), other.len());
self.list.append(other);
}
/// Remove the `index` element.
///
/// See [`Vec::remove`].
pub fn remove(&mut self, index: usize) -> T {
let r = self.list.remove(index);
self.changes.removed(index, 1);
r
}
/// Remove the last element from the vec.
///
/// See [`Vec::pop`].
pub fn pop(&mut self) -> Option<T> {
if self.is_empty() {
None
} else {
Some(self.remove(self.len() - 1))
}
}
/// Shortens the vector, keeping the first `len` elements and dropping the rest.
///
/// See [`Vec::truncate`].
pub fn truncate(&mut self, len: usize) {
if len < self.len() {
let count = self.len() - len;
self.changes.removed(len, count);
}
self.list.truncate(len);
}
/// Removes an element from the vector and returns it.
///
/// See [`Vec::swap_remove`].
pub fn swap_remove(&mut self, index: usize) -> T {
let r = self.list.swap_remove(index);
self.changes.removed(index, 1);
self.changes.moved(self.list.len() - 1, index);
r
}
/// Removes all elements.
///
/// See [`Vec::clear`].
pub fn clear(&mut self) {
if !self.is_empty() {
self.clear();
self.changes.cleared();
}
}
/// Retains only the elements specified by the predicate, passing a mutable reference to it.
///
/// See [`Vec::retain_mut`] for more details.
pub fn retain<F>(&mut self, mut f: F)
where
F: FnMut(&mut T) -> bool,
{
let mut i = 0;
self.list.retain_mut(|it| {
let retain = f(it);
if retain {
i += 1;
} else {
self.changes.removed(i, 1);
}
retain
})
}
/// Removes the specified range from the vector in bulk, returning all removed elements as an iterator.
///
/// See [`Vec::drain`].
pub fn drain<R>(&mut self, range: R) -> std::vec::Drain<'_, T>
where
R: ops::RangeBounds<usize>,
{
let range = std_slice_range(range, ..self.len());
let r = self.list.drain(range.clone());
if !range.is_empty() {
self.changes.removed(range.start, range.len());
}
r
}
/// Resizes the Vec in-place so that len is equal to `new_len`.
///
/// See [`Vec::resize`].
pub fn resize(&mut self, new_len: usize, value: T) {
if new_len <= self.len() {
self.truncate(new_len);
} else {
let count = new_len - self.len();
self.changes.inserted(self.len(), count);
self.list.resize(new_len, value);
}
}
/// Clones and appends all elements in a slice to the Vec.
///
/// See [`Vec::extend_from_slice`].
pub fn extend_from_slice(&mut self, other: &[T]) {
if !other.is_empty() {
self.changes.inserted(self.len(), other.len());
}
self.list.extend_from_slice(other);
}
/// Copies elements from `src` range to the end of the vector.
pub fn extend_from_within<R>(&mut self, src: R)
where
R: ops::RangeBounds<usize>,
{
let src = std_slice_range(src, ..self.len());
let index = self.len();
self.list.extend_from_within(src.clone());
if !src.is_empty() {
self.changes.inserted(index, src.len());
}
}
/// Move the element `from` index `to` index.
///
/// If `from < to` this is the same as `self.insert(to - 1, self.remove(from))`, otherwise
/// is the same as `self.insert(to, self.remove(from))`, but the change is tracked
/// as a single [`VecChange::Move`].
pub fn reinsert(&mut self, from: usize, mut to: usize) {
if from != to {
if from < to {
to -= 1;
}
let el = self.list.remove(from);
self.list.insert(to, el);
self.changes.moved(from, to);
} else {
// assert contained
let _ = &self.list[to];
}
}
/// Mutate the `index`.
///
/// This logs a [`VecChange::Remove`] and [`VecChange::Insert`] for the `index`, if it is valid.
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
let r = self.list.get_mut(index);
if r.is_some() {
self.changes.removed(index, 1);
self.changes.inserted(index, 1);
}
r
}
/// Mutate the `range`.
///
/// This logs a [`VecChange::Remove`] and [`VecChange::Insert`] for the `range`, if it is valid.
pub fn slice_mut<R>(&mut self, range: R) -> &mut [T]
where
R: ops::RangeBounds<usize>,
{
let range = std_slice_range(range, ..self.len());
let r = &mut self.list[range.clone()];
let count = range.len();
if count > 0 {
self.changes.removed(range.start, count);
self.changes.inserted(range.start, count);
}
r
}
/// Changes applied in the last var update.
///
/// If the variable is new and this is empty assume the entire vector was replaced (same as [`VecChange::Clear`]).
pub fn changes(&self) -> &[VecChange] {
if self.changes.update_id == VARS.update_id() {
&self.changes.changes
} else {
&[]
}
}
}
impl<T: VarValue> Extend<T> for ObservableVec<T> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
let index = self.len();
self.list.extend(iter);
let count = self.len() - index;
if count > 0 {
self.changes.inserted(index, count);
}
}
}
impl<T: VarValue> From<Vec<T>> for ObservableVec<T> {
fn from(value: Vec<T>) -> Self {
Self {
list: value,
changes: VecChanges::new(),
}
}
}
impl<T: VarValue> From<ObservableVec<T>> for Vec<T> {
fn from(value: ObservableVec<T>) -> Self {
value.list
}
}
/// Represents a change in a [`ObservableVec`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VecChange {
/// Elements removed.
Remove {
/// Index of the first element removed, at the time of removal.
index: usize,
/// Number of elements removed.
count: usize,
},
/// Elements inserted.
Insert {
/// Index of the first element inserted, at the time of insertion.
///
/// In [`ObservableVec::changes`] the index and count can be used to select
/// the new elements in the vec because if any (re)move is made after insert the
/// changes are collapsed to a `Clear`.
index: usize,
/// Number of elements inserted.
count: usize,
},
/// Element removed an reinserted.
Move {
/// Index the element was first at.
from_index: usize,
/// Index the element was reinserted after removal.
to_index: usize,
},
/// All elements removed/replaced.
Clear,
}
#[derive(Debug, PartialEq)]
struct VecChanges {
changes: Vec<VecChange>,
update_id: VarUpdateId,
}
impl Clone for VecChanges {
fn clone(&self) -> Self {
let update_id = VARS.update_id();
if self.update_id == update_id {
Self {
changes: self.changes.clone(),
update_id,
}
} else {
Self {
changes: vec![],
update_id,
}
}
}
}
impl VecChanges {
const fn new() -> Self {
Self {
changes: vec![],
update_id: VarUpdateId::never(),
}
}
pub fn inserted(&mut self, i: usize, n: usize) {
let update_id = VARS.update_id();
if self.update_id != update_id {
self.changes.clear();
self.changes.push(VecChange::Insert { index: i, count: n });
self.update_id = update_id;
} else if self.changes != [VecChange::Clear] {
if let Some(VecChange::Insert { index, count }) = self.changes.last_mut() {
if i >= *index && i <= *index + *count {
// new insert inside previous
*count += n;
return;
} else {
// insert indexes need to be patched.
self.changes.clear();
self.changes.push(VecChange::Clear);
return;
}
}
self.changes.push(VecChange::Insert { index: i, count: n });
}
}
pub fn moved(&mut self, f: usize, t: usize) {
let update_id = VARS.update_id();
if self.update_id != update_id {
self.changes.clear();
self.changes.push(VecChange::Move {
from_index: f,
to_index: t,
});
self.update_id = update_id;
} else if self.changes != [VecChange::Clear] {
self.changes.push(VecChange::Move {
from_index: f,
to_index: t,
});
}
}
pub fn removed(&mut self, i: usize, n: usize) {
let update_id = VARS.update_id();
if self.update_id != update_id {
self.changes.clear();
self.changes.push(VecChange::Remove { index: i, count: n });
self.update_id = update_id;
} else if self.changes != [VecChange::Clear] {
if let Some(last) = self.changes.last_mut() {
match last {
VecChange::Remove { index, count } => {
let s = i;
let e = i + n;
if s <= *index && e > *index {
// new remove contains previous remove.
*index = s;
*count += n;
return;
}
}
VecChange::Insert { .. } => {
// insert indexes need to be patched.
self.changes.clear();
self.changes.push(VecChange::Clear);
return;
}
_ => {}
}
}
self.changes.push(VecChange::Remove { index: i, count: n });
}
}
pub fn cleared(&mut self) {
self.changes.clear();
self.changes.push(VecChange::Clear);
self.update_id = VARS.update_id();
}
}
// See <https://github.com/rust-lang/rust/issues/76393>
#[track_caller]
#[must_use]
fn std_slice_range<R>(range: R, bounds: ops::RangeTo<usize>) -> ops::Range<usize>
where
R: ops::RangeBounds<usize>,
{
let len = bounds.end;
let start: ops::Bound<&usize> = range.start_bound();
let start = match start {
ops::Bound::Included(&start) => start,
ops::Bound::Excluded(start) => start.checked_add(1).unwrap(),
ops::Bound::Unbounded => 0,
};
let end: ops::Bound<&usize> = range.end_bound();
let end = match end {
ops::Bound::Included(end) => end.checked_add(1).unwrap(),
ops::Bound::Excluded(&end) => end,
ops::Bound::Unbounded => len,
};
if start > end {
panic!("invalid range {start}..{end}");
}
if end > len {
panic!("invalid range {start}..{end}");
}
ops::Range { start, end }
}
#[cfg(test)]
mod tests {
use zero_ui_app::APP;
use super::*;
#[test]
fn basic_usage() {
let mut app = APP.minimal().run_headless(false);
let list = var(ObservableVec::<u32>::new());
list.modify(|a| {
a.to_mut().push(32);
});
app.update_observe(
|| {
assert!(list.is_new());
list.with_new(|l| {
assert_eq!(&[32], &l[..]);
assert_eq!(&[VecChange::Insert { index: 0, count: 1 }], l.changes());
});
},
false,
)
.assert_wait();
list.modify(|a| {
a.to_mut().push(33);
});
app.update_observe(
|| {
assert!(list.is_new());
list.with_new(|l| {
assert_eq!(&[32, 33], &l[..]);
assert_eq!(&[VecChange::Insert { index: 1, count: 1 }], l.changes());
});
},
false,
)
.assert_wait();
}
}

View File

@ -1,5 +1,5 @@
[package]
name = "zero-ui-widget"
name = "zero-ui-wgt"
version = "0.1.0"
authors = ["Samuel Guerra <sam.rodr.g@gmail.com>", "Well <well-r@hotmail.com>"]
edition = "2021"
@ -21,14 +21,18 @@ 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-color = { path = "../zero-ui-color" }
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" }
zero-ui-task = { path = "../zero-ui-task" }
zero-ui-txt = { path = "../zero-ui-txt" }
zero-ui-unique_id = { path = "../zero-ui-unique_id" }
paste = "1"
tracing = "0.1"
serde = { version = "1", features = ["derive"] }
[package.metadata.docs.rs]
rustdoc-args = ["--html-in-header", "zero-ui-widget/doc/html-in-header.html"]
rustdoc-args = ["--html-in-header", "zero-ui-wgt/doc/html-in-header.html"]

View File

@ -0,0 +1,209 @@
use zero_ui_app::widget::border::{BORDER, BORDER_ALIGN_VAR, BORDER_OVER_VAR, CORNER_RADIUS_FIT_VAR, CORNER_RADIUS_VAR};
use crate::prelude::*;
/// Border control properties.
#[widget_mixin]
pub struct BorderMix<P>(P);
/// Corner radius of widget and inner widgets.
///
/// The [`Default`] value is calculated to fit inside the parent widget corner curve, see [`corner_radius_fit`].
///
/// [`Default`]: zero_ui_layout::units::Length::Default
/// [`corner_radius_fit`]: fn@corner_radius_fit
#[property(CONTEXT, default(CORNER_RADIUS_VAR), widget_impl(BorderMix<P>))]
pub fn corner_radius(child: impl UiNode, radius: impl IntoVar<CornerRadius>) -> impl UiNode {
let child = match_node(child, move |child, op| {
if let UiNodeOp::Layout { wl, final_size } = op {
*final_size = BORDER.with_corner_radius(|| child.layout(wl));
}
});
with_context_var(child, CORNER_RADIUS_VAR, radius)
}
/// Defines how the [`corner_radius`] is computed for each usage.
///
/// Nesting borders with round corners need slightly different radius values to perfectly fit, the [`BORDER`]
/// coordinator can adjusts the radius inside each border to match the inside curve of the border.
///
/// Sets the [`CORNER_RADIUS_FIT_VAR`].
///
/// [`corner_radius`]: fn@corner_radius
/// [`BORDER`]: zero_ui_app::widget::border::BORDER
#[property(CONTEXT, default(CORNER_RADIUS_FIT_VAR), widget_impl(BorderMix<P>))]
pub fn corner_radius_fit(child: impl UiNode, fit: impl IntoVar<CornerRadiusFit>) -> impl UiNode {
with_context_var(child, CORNER_RADIUS_FIT_VAR, fit)
}
/// Position of a widget borders in relation to the widget fill.
///
/// This property defines how much the widget's border offsets affect the layout of the fill content, by default
/// (0%) the fill content stretchers *under* the borders and is clipped by the [`corner_radius`], in the other end
/// of the scale (100%), the fill content is positioned *inside* the borders and clipped by the adjusted [`corner_radius`]
/// that fits the insider of the inner most border.
///
/// Note that widget's content is always *inside* the borders, this property only affects the *fill* properties content, such as a
/// the image in a background image.
///
/// Fill property implementers, see [`fill_node`], a helper function for quickly implementing support for `border_align`.
///
/// Sets the [`BORDER_ALIGN_VAR`].
///
/// [`corner_radius`]: fn@corner_radius
#[property(CONTEXT, default(BORDER_ALIGN_VAR), widget_impl(BorderMix<P>))]
pub fn border_align(child: impl UiNode, align: impl IntoVar<FactorSideOffsets>) -> impl UiNode {
with_context_var(child, BORDER_ALIGN_VAR, align)
}
/// If the border is rendered over the fill and child visuals.
///
/// Is `true` by default, if set to `false` the borders will render under the fill. Note that
/// this means the border will be occluded by the *background* if [`border_align`] is not set to `1.fct()`.
///
/// Sets the [`BORDER_OVER_VAR`].
///
/// [`border_align`]: fn@border_align
#[property(CONTEXT, default(BORDER_OVER_VAR), widget_impl(BorderMix<P>))]
pub fn border_over(child: impl UiNode, over: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, BORDER_OVER_VAR, over)
}
/// Widget border.
///
/// Defines a widget border, it coordinates with any other border in the widget to fit inside or outside the
/// other borders, it also works with the [`corner_radius`] property drawing round corners if configured.
///
/// This property disables inline layout for the widget.
///
/// # Examples
///
/// A border of width `1.dip()`, solid color `BLUE` in all border sides and corner radius `4.dip()`.
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// border = 1, colors::BLUE;
/// corner_radius = 4;
/// }
/// # }}
/// ```
///
/// A border that sets each border line to a different width `top: 1, right: 2, bottom: 3, left: 4`, each corner
/// radius to a different size `top_left: 1x1, top_right: 2x2, bottom_right: 3x3, bottom_left: 4x4` and each border
/// line to a different style and color.
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// border = {
/// widths: (1, 2, 3, 4),
/// sides: BorderSides::new(
/// BorderSide::solid(colors::RED),
/// BorderSide::dashed(colors::GREEN),
/// BorderSide::dotted(colors::BLUE),
/// BorderSide::double(colors::YELLOW),
/// ),
/// };
/// corner_radius = (1, 2, 3, 4);
/// }
/// # }}
/// ```
///
/// ## Multiple Borders
///
/// The border fits in with other borders in the widget, in this example we declare a
/// new border property by copying the signature of this one:
///
/// ```
/// use zero_ui_wgt::prelude::*;
///
/// /// Another border property.
/// #[property(BORDER, default(0, BorderStyle::Hidden))]
/// pub fn my_border(child: impl UiNode, widths: impl IntoVar<SideOffsets>, sides: impl IntoVar<BorderSides>) -> impl UiNode {
/// zero_ui_wgt::border(child, widths, sides)
/// }
/// #
/// # fn main() { }
/// ```
///
/// Now we can set two borders in the same widget:
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// border = 4, colors::RED;
/// my_border = 4, colors::GREEN;
/// corner_radius = 8;
/// }
/// # }}
/// ```
///
/// This will render a `RED` border around a `GREEN` one, the inner border will fit perfectly inside the outer one,
/// the `corner_radius` defines the the outer radius, the inner radius is computed automatically to fit.
///
/// Note that because both borders have the same [`NestGroup::BORDER`] the position they are declared in the widget matters:
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// my_border = 4, colors::GREEN;
/// border = 4, colors::RED;
/// corner_radius = 8;
/// }
/// # }}
/// ```
///
/// Now the `GREEN` border is around the `RED`.
///
/// You can adjust the nest group to cause a border to always be outside or inside:
///
/// ```
/// use zero_ui_wgt::prelude::*;
///
/// /// Border that is always around the other borders.
/// #[property(BORDER-1, default(0, BorderStyle::Hidden))]
/// pub fn outside_border(child: impl UiNode, widths: impl IntoVar<SideOffsets>, sides: impl IntoVar<BorderSides>) -> impl UiNode {
/// zero_ui_wgt::border(child, widths, sides)
/// }
///
/// /// Border that is always inside the other borders.
/// #[property(BORDER+1, default(0, BorderStyle::Hidden))]
/// pub fn inside_border(child: impl UiNode, widths: impl IntoVar<SideOffsets>, sides: impl IntoVar<BorderSides>) -> impl UiNode {
/// zero_ui_wgt::border(child, widths, sides)
/// }
/// #
/// # fn main() { }
/// ```
///
/// [`corner_radius`]: fn@corner_radius
/// [`NestGroup::BORDER`]: zero_ui_app::widget::builder::NestGroup::BORDER
#[property(BORDER, default(0, BorderStyle::Hidden), widget_impl(BorderMix<P>))]
pub fn border(child: impl UiNode, widths: impl IntoVar<SideOffsets>, sides: impl IntoVar<BorderSides>) -> impl UiNode {
let sides = sides.into_var();
let mut corners = PxCornerRadius::zero();
border_node(
child,
widths,
match_node_leaf(move |op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&sides);
}
UiNodeOp::Measure { desired_size, .. } => {
*desired_size = LAYOUT.constraints().fill_size();
}
UiNodeOp::Layout { final_size, .. } => {
corners = BORDER.border_radius();
*final_size = LAYOUT.constraints().fill_size();
}
UiNodeOp::Render { frame } => {
let (rect, offsets) = BORDER.border_layout();
if !rect.size.is_empty() {
frame.push_border(rect, offsets, sides.get(), corners);
}
}
_ => {}
}),
)
}

View File

@ -0,0 +1,83 @@
use crate::prelude::*;
/// Clips the widget child to the area of the widget when set to `true`.
///
/// Any content rendered outside the widget inner bounds is clipped, hit test shapes are also clipped. The clip is
/// rectangular and can have rounded corners if [`corner_radius`] is set. If the widget is inlined during layout the first
/// row advance and last row trail are also clipped.
///
/// # Examples
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// background_color = rgb(255, 0, 0);
/// size = (200, 300);
/// corner_radius = 5;
/// clip_to_bounds = true;
/// child = Container! {
/// background_color = rgb(0, 255, 0);
/// // fixed size ignores the layout available size.
/// size = (1000, 1000);
/// child = Text!("1000x1000 green clipped to 200x300");
/// };
/// }
/// # }}
/// ```
///
/// [`corner_radius`]: fn@crate::corner_radius
#[property(FILL, default(false))]
pub fn clip_to_bounds(child: impl UiNode, clip: impl IntoVar<bool>) -> impl UiNode {
let clip = clip.into_var();
let mut corners = PxCornerRadius::zero();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.layout().render();
}
UiNodeOp::Update { .. } => {
if clip.is_new() {
WIDGET.layout().render();
}
}
UiNodeOp::Layout { wl, final_size } => {
let bounds = child.layout(wl);
if clip.get() {
let c = BORDER.border_radius();
if c != corners {
corners = c;
WIDGET.render();
}
}
*final_size = bounds;
}
UiNodeOp::Render { frame } => {
if clip.get() {
frame.push_clips(
|c| {
let wgt_bounds = WIDGET.bounds();
let bounds = PxRect::from_size(wgt_bounds.inner_size());
if corners != PxCornerRadius::zero() {
c.push_clip_rounded_rect(bounds, corners, false, true);
} else {
c.push_clip_rect(bounds, false, true);
}
if let Some(inline) = wgt_bounds.inline() {
for r in inline.negative_space().iter() {
c.push_clip_rect(*r, true, true);
}
};
},
|f| child.render(f),
);
} else {
child.render(frame);
}
}
_ => {}
})
}

View File

@ -0,0 +1,9 @@
use zero_ui_color::COLOR_SCHEME_VAR;
use crate::prelude::*;
/// Defines the preferred color scheme in the widget and descendants.
#[property(CONTEXT, default(COLOR_SCHEME_VAR))]
pub fn color_scheme(child: impl UiNode, pref: impl IntoVar<ColorScheme>) -> impl UiNode {
with_context_var(child, COLOR_SCHEME_VAR, pref)
}

View File

@ -0,0 +1,127 @@
use std::fmt;
use crate::prelude::*;
/// Hit-test visibility properties.
///
/// Mixin defines hit-test control state probing properties for all widgets.
#[widget_mixin]
pub struct HitTestMix<P>(P);
/// Defines if and how a widget is hit-tested.
///
/// See [`hit_test_mode`](fn@hit_test_mode) for more details.
#[derive(Copy, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum HitTestMode {
/// Widget is never hit.
///
/// This mode affects the entire UI branch, if set it disables hit-testing for the widget and all its descendants.
Disabled,
/// Widget is hit by any point that intersects the transformed inner bounds rectangle. If the widget is inlined
/// excludes the first row advance and the last row trailing space.
Bounds,
/// Default mode.
///
/// Same as `Bounds`, but also excludes the outside of rounded corners.
#[default]
RoundedBounds,
/// Every render primitive used for rendering the widget is hit-testable, the widget is hit only by
/// points that intersect visible parts of the render primitives.
///
/// Note that not all primitives implement pixel accurate hit-testing.
Visual,
}
impl HitTestMode {
/// Returns `true` if is any mode other then [`Disabled`].
///
/// [`Disabled`]: Self::Disabled
pub fn is_hit_testable(&self) -> bool {
!matches!(self, Self::Disabled)
}
/// Read-only context var with the contextual mode.
pub fn var() -> impl Var<HitTestMode> {
HIT_TEST_MODE_VAR.read_only()
}
}
impl fmt::Debug for HitTestMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "HitTestMode::")?;
}
match self {
Self::Disabled => write!(f, "Disabled"),
Self::Bounds => write!(f, "Bounds"),
Self::RoundedBounds => write!(f, "RoundedBounds"),
Self::Visual => write!(f, "Visual"),
}
}
}
impl_from_and_into_var! {
fn from(default_or_disabled: bool) -> HitTestMode {
if default_or_disabled {
HitTestMode::default()
} else {
HitTestMode::Disabled
}
}
}
context_var! {
static HIT_TEST_MODE_VAR: HitTestMode = HitTestMode::default();
}
/// Defines how the widget is hit-tested.
///
/// Hit-testing determines if a point intersects with the widget, the most common hit-test point is the mouse pointer.
/// By default widgets are hit by any point inside the widget area, excluding the outer corners if [`corner_radius`] is set,
/// this is very efficient, but assumes that the widget is *filled*, if the widget has visual *holes* the user may be able
/// to see another widget underneath but be unable to click on it.
///
/// If you have a widget with a complex shape or with *holes*, set this property to [`HitTestMode::Visual`] to enable the full
/// hit-testing power where all render primitives and clips used to render the widget are considered during hit-testing.
///
/// [`hit_testable`]: fn@hit_testable
/// [`corner_radius`]: fn@crate::corner_radius
#[property(CONTEXT, default(HIT_TEST_MODE_VAR), widget_impl(HitTestMix<P>))]
pub fn hit_test_mode(child: impl UiNode, mode: impl IntoVar<HitTestMode>) -> impl UiNode {
let child = match_node(child, |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&HitTestMode::var());
}
UiNodeOp::Render { frame } => match HitTestMode::var().get() {
HitTestMode::Disabled => {
frame.with_hit_tests_disabled(|frame| child.render(frame));
}
HitTestMode::Visual => frame.with_auto_hit_test(true, |frame| child.render(frame)),
_ => frame.with_auto_hit_test(false, |frame| child.render(frame)),
},
UiNodeOp::RenderUpdate { update } => {
update.with_auto_hit_test(matches!(HitTestMode::var().get(), HitTestMode::Visual), |update| {
child.render_update(update)
});
}
_ => {}
});
with_context_var(
child,
HIT_TEST_MODE_VAR,
merge_var!(HIT_TEST_MODE_VAR, mode.into_var(), |&a, &b| match (a, b) {
(HitTestMode::Disabled, _) | (_, HitTestMode::Disabled) => HitTestMode::Disabled,
(_, b) => b,
}),
)
}
/// If the widget is visible for hit-tests.
///
/// This property is used only for probing the state. You can set the state using
/// the [`hit_test_mode`] property.
///
/// [`hit_testable`]: fn@hit_testable
/// [`hit_test_mode`]: fn@hit_test_mode
#[property(EVENT, widget_impl(HitTestMix<P>))]
pub fn is_hit_testable(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
bind_is_state(child, HIT_TEST_MODE_VAR.map(|m| m.is_hit_testable()), state)
}

View File

@ -0,0 +1,320 @@
use zero_ui_app::widget::info;
use crate::prelude::*;
/// Interactivity properties.
///
/// Mixin defines enabled and enabled state probing properties for interactive widgets.
#[widget_mixin]
pub struct InteractivityMix<P>(P);
context_var! {
static IS_ENABLED_VAR: bool = true;
}
/// If default interaction is allowed in the widget and its descendants.
///
/// This property sets the interactivity of the widget to [`ENABLED`] or [`DISABLED`], to probe the enabled state in `when` clauses
/// use [`is_enabled`] or [`is_disabled`]. To probe the a widget's state use [`interactivity`] value.
///
/// # Interactivity
///
/// Every widget has an [`interactivity`] value, it defines two *tiers* of disabled, the normal disabled blocks the default actions
/// of the widget, but still allows some interactions, such as a different cursor on hover or event an error tool-tip on click, the
/// second tier blocks all interaction with the widget. This property controls the *normal* disabled, to fully block interaction use
/// the [`interactive`] property.
///
/// # Disabled Visual
///
/// Widgets that are interactive should visually indicate when the normal interactions are disabled, you can use the [`is_disabled`]
/// state property in a when block to implement the *visually disabled* appearance of a widget.
///
/// The visual cue for the disabled state is usually a reduced contrast from content and background by *graying-out* the text and applying a
/// grayscale filter for image content. You should also consider adding *disabled interactions* that inform the user when the widget will be
/// enabled.
///
/// # Implicit
///
/// This property is included in all widgets by default, you don't need to import it to use it.
///
/// [`ENABLED`]: zero_ui_app::widget::info::Interactivity::ENABLED
/// [`DISABLED`]: zero_ui_app::widget::info::Interactivity::DISABLED
/// [`interactivity`]: zero_ui_app::widget::info::WidgetInfo::interactivity
/// [`interactive`]: fn@interactive
/// [`is_enabled`]: fn@is_enabled
/// [`is_disabled`]: fn@is_disabled
#[property(CONTEXT, default(true), widget_impl(InteractivityMix<P>))]
pub fn enabled(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
let child = match_node(
child,
clmv!(enabled, |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&enabled);
}
UiNodeOp::Info { info } => {
if !enabled.get() {
info.push_interactivity(Interactivity::DISABLED);
}
}
_ => {}
}),
);
with_context_var(child, IS_ENABLED_VAR, merge_var!(IS_ENABLED_VAR, enabled, |&a, &b| a && b))
}
/// Defines if any interaction is allowed in the widget and its descendants.
///
/// This property sets the interactivity of the widget to [`BLOCKED`] when `false`, widgets with blocked interactivity do not
/// receive any interaction event and behave like a background visual. To probe the widget state use [`interactivity`] value.
///
/// This property *enables* and *disables* interaction with the widget and its descendants without causing
/// a visual change like [`enabled`], it also blocks "disabled" interactions such as a different cursor or tool-tip for disabled buttons,
/// its use cases are more advanced then [`enabled`], it is mostly used when large parts of the screen are "not ready".
///
/// Note that this affects the widget where it is set and descendants, to disable interaction only in the widgets
/// inside `child` use the [`base::nodes::interactive_node`].
///
/// [`enabled`]: fn@enabled
/// [`BLOCKED`]: Interactivity::BLOCKED
/// [`interactivity`]: zero_ui_app::widget::info::WidgetInfo::interactivity
/// [`base::nodes::interactive_node`]: zero_ui_app::widget::base::nodes::interactive_node
#[property(CONTEXT, default(true), widget_impl(InteractivityMix<P>))]
pub fn interactive(child: impl UiNode, interactive: impl IntoVar<bool>) -> impl UiNode {
let interactive = interactive.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&interactive);
}
UiNodeOp::Info { info } => {
if !interactive.get() {
info.push_interactivity(Interactivity::BLOCKED);
}
}
_ => {}
})
}
/// If the widget is enabled for interaction.
///
/// This property is used only for probing the state. You can set the state using
/// the [`enabled`] property.
///
/// [`enabled`]: fn@enabled
/// [`WidgetInfo::allow_interaction`]: crate::widget_info::WidgetInfo::allow_interaction
#[property(EVENT, widget_impl(InteractivityMix<P>))]
pub fn is_enabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
vis_enabled_eq_state(child, state, true)
}
/// If the widget is disabled for interaction.
///
/// This property is used only for probing the state. You can set the state using
/// the [`enabled`] property.
///
/// This is the same as `!self.is_enabled`.
///
/// [`enabled`]: fn@enabled
#[property(EVENT, widget_impl(InteractivityMix<P>))]
pub fn is_disabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
vis_enabled_eq_state(child, state, false)
}
fn vis_enabled_eq_state(child: impl UiNode, state: impl IntoVar<bool>, expected: bool) -> impl UiNode {
event_is_state(child, state, true, info::INTERACTIVITY_CHANGED_EVENT, move |args| {
if let Some((_, new)) = args.vis_enabled_change(WIDGET.id()) {
Some(new.is_vis_enabled() == expected)
} else {
None
}
})
}
event_property! {
/// Widget interactivity changed.
///
/// Note that there are multiple specific events for interactivity changes, [`on_enable`], [`on_disable`], [`on_block`] and [`on_unblock`]
/// are some of then.
///
/// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
/// from `None`, this initial event can be detected using the [`is_new`] method in the args.
///
/// [`on_enable`]: fn@on_enable
/// [`on_disable`]: fn@on_disable
/// [`on_block`]: fn@on_block
/// [`on_unblock`]: fn@on_unblock
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn interactivity_changed {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
}
/// Widget was enabled or disabled.
///
/// Note that this event tracks the *actual* enabled status of the widget, not the *visually enabled* status,
/// see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
/// from `None`, this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_interactivity_changed`] for a more general interactivity event.
///
/// [`on_interactivity_changed`]: fn@on_interactivity_changed
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn enabled_changed {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.enabled_change(WIDGET.id()).is_some(),
}
/// Widget changed to enabled or disabled visuals.
///
/// Note that this event tracks the *visual* enabled status of the widget, not the *actual* status, the widget may
/// still be blocked, see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
/// from `None`, this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_interactivity_changed`] for a more general interactivity event.
///
/// [`on_interactivity_changed`]: fn@on_interactivity_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn vis_enabled_changed {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.vis_enabled_change(WIDGET.id()).is_some(),
}
/// Widget interactions where blocked or unblocked.
///
/// Note that blocked widgets may still be visually enabled, see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree, this is because the interactivity *changed*
/// from `None`, this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_interactivity_changed`] for a more general interactivity event.
///
/// [`on_interactivity_changed`]: fn@on_interactivity_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn blocked_changed {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.blocked_change(WIDGET.id()).is_some(),
}
/// Widget normal interactions now enabled.
///
/// Note that this event tracks the *actual* enabled status of the widget, not the *visually enabled* status,
/// see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree if it starts enabled,
/// this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_enabled_changed`] for a more general event.
///
/// [`on_enabled_changed`]: fn@on_enabled_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn enable {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.is_enable(WIDGET.id()),
}
/// Widget normal interactions now disabled.
///
/// Note that this event tracks the *actual* enabled status of the widget, not the *visually enabled* status,
/// see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree if it starts disabled,
/// this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_enabled_changed`] for a more general event.
///
/// [`on_enabled_changed`]: fn@on_enabled_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn disable {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.is_disable(WIDGET.id()),
}
/// Widget now using the enabled visuals.
///
/// Note that this event tracks the *visual* enabled status of the widget, not the *actual* status, the widget may
/// still be blocked, see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree if it starts visually enabled,
/// this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_vis_enabled_changed`] for a more general event.
///
/// [`on_vis_enabled_changed`]: fn@on_vis_enabled_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn vis_enable {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.is_vis_enable(WIDGET.id()),
}
/// Widget now using the disabled visuals.
///
/// Note that this event tracks the *visual* enabled status of the widget, not the *actual* status, the widget may
/// still be blocked, see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree if it starts visually disabled,
/// this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_vis_enabled_changed`] for a more general event.
///
/// [`on_vis_enabled_changed`]: fn@on_vis_enabled_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn vis_disable {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.is_vis_disable(WIDGET.id()),
}
/// Widget interactions now blocked.
///
/// Note that blocked widgets may still be visually enabled, see [`Interactivity`] for more details.
///
/// Note that an event is received when the widget first initializes in the widget info tree if it starts blocked,
/// this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_blocked_changed`] for a more general event.
///
/// [`on_blocked_changed`]: fn@on_blocked_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn block {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.is_block(WIDGET.id()),
}
/// Widget interactions now unblocked.
///
/// Note that the widget may still be disabled.
///
/// Note that an event is received when the widget first initializes in the widget info tree if it starts unblocked,
/// this initial event can be detected using the [`is_new`] method in the args.
///
/// See [`on_blocked_changed`] for a more general event.
///
/// [`on_blocked_changed`]: fn@on_blocked_changed
/// [`Interactivity`]: zero_ui_app::widget::info::Interactivity
/// [`is_new`]: info::InteractivityChangedArgs::is_new
pub fn unblock {
event: info::INTERACTIVITY_CHANGED_EVENT,
args: info::InteractivityChangedArgs,
filter: |a| a.is_unblock(WIDGET.id()),
}
}

View File

@ -0,0 +1,242 @@
use std::fmt;
use zero_ui_layout::context::DIRECTION_VAR;
use crate::prelude::*;
/// Margin space around the widget.
///
/// This property adds side offsets to the widget inner visual, it will be combined with the other
/// layout properties of the widget to define the inner visual position and widget size.
///
/// This property disables inline layout for the widget.
///
/// # Examples
///
/// ```
/// # macro_rules! _demo { () => {
/// Button! {
/// margin = 10;
/// child = Text!("Click Me!")
/// }
/// # }}
/// ```
///
/// In the example the button has `10` layout pixels of space in all directions around it. You can
/// also control each side in specific:
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// child = Button! {
/// margin = (10, 5.pct());
/// child = Text!("Click Me!")
/// };
/// margin = (1, 2, 3, 4);
/// }
/// # }}
/// ```
///
/// In the example the button has `10` pixels of space above and bellow and `5%` of the container width to the left and right.
/// The container itself has margin of `1` to the top, `2` to the right, `3` to the bottom and `4` to the left.
///
#[property(LAYOUT, default(0))]
pub fn margin(child: impl UiNode, margin: impl IntoVar<SideOffsets>) -> impl UiNode {
let margin = margin.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&margin);
}
UiNodeOp::Measure { wm, desired_size } => {
let margin = margin.layout();
let size_increment = PxSize::new(margin.horizontal(), margin.vertical());
*desired_size = LAYOUT.with_constraints(LAYOUT.constraints().with_less_size(size_increment), || wm.measure_block(child));
desired_size.width += size_increment.width;
desired_size.height += size_increment.height;
}
UiNodeOp::Layout { wl, final_size } => {
let margin = margin.layout();
let size_increment = PxSize::new(margin.horizontal(), margin.vertical());
*final_size = LAYOUT.with_constraints(LAYOUT.constraints().with_less_size(size_increment), || child.layout(wl));
let mut translate = PxVector::zero();
final_size.width += size_increment.width;
translate.x = margin.left;
final_size.height += size_increment.height;
translate.y = margin.top;
wl.translate(translate);
}
_ => {}
})
}
/// Aligns the widget within the available space.
///
/// This property disables inline layout for the widget.
///
/// # Examples
///
/// ```
/// # macro_rules! _demo { () => {
/// Container! {
/// child = Button! {
/// align = Align::TOP;
/// child = Text!("Click Me!")
/// };
/// }
/// # }}
/// ```
///
/// In the example the button is positioned at the top-center of the container. See [`Align`] for
/// more details.
#[property(LAYOUT, default(Align::FILL))]
pub fn align(child: impl UiNode, alignment: impl IntoVar<Align>) -> impl UiNode {
let alignment = alignment.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&alignment);
}
UiNodeOp::Measure { wm, desired_size } => {
let align = alignment.get();
let child_size = LAYOUT.with_constraints(align.child_constraints(LAYOUT.constraints()), || wm.measure_block(child));
*desired_size = align.measure(child_size, LAYOUT.constraints());
}
UiNodeOp::Layout { wl, final_size } => {
let align = alignment.get();
let child_size = LAYOUT.with_constraints(align.child_constraints(LAYOUT.constraints()), || child.layout(wl));
let (size, offset, baseline) = align.layout(child_size, LAYOUT.constraints(), LAYOUT.direction());
wl.translate(offset);
if baseline {
wl.translate_baseline(true);
}
*final_size = size;
}
_ => {}
})
}
/// If the layout direction is right-to-left.
///
/// The `state` is bound to [`DIRECTION_VAR`].
#[property(LAYOUT)]
pub fn is_rtl(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
bind_is_state(child, DIRECTION_VAR.map(|s| s.is_rtl()), state)
}
/// If the layout direction is left-to-right.
///
/// The `state` is bound to [`DIRECTION_VAR`].
#[property(LAYOUT)]
pub fn is_ltr(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
bind_is_state(child, DIRECTION_VAR.map(|s| s.is_ltr()), state)
}
/// Inline mode explicitly selected for a widget.
///
/// See the [`inline`] property for more details.
///
/// [`inline`]: fn@inline
#[derive(Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum InlineMode {
/// Widget does inline if requested by the parent widget layout and is composed only of properties that support inline.
///
/// This is the default behavior.
#[default]
Allow,
/// Widget always does inline.
///
/// If the parent layout does not setup an inline layout environment the widget it-self will. This
/// can be used to force the inline visual, such as background clipping or any other special visual
/// that is only enabled when the widget is inlined.
///
/// Note that the widget will only inline if composed only of properties that support inline.
Inline,
/// Widget disables inline.
///
/// If the parent widget requests inline the request does not propagate for child nodes and
/// inline is disabled on the widget.
Block,
}
impl fmt::Debug for InlineMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "InlineMode::")?;
}
match self {
Self::Allow => write!(f, "Allow"),
Self::Inline => write!(f, "Inline"),
Self::Block => write!(f, "Block"),
}
}
}
impl_from_and_into_var! {
fn from(inline: bool) -> InlineMode {
if inline {
InlineMode::Inline
} else {
InlineMode::Block
}
}
}
/// Enforce an inline mode on the widget.
///
/// Set to [`InlineMode::Inline`] to use the inline layout and visual even if the widget
/// is not in an inlining parent. Note that the widget will still not inline if it has properties
/// that disable inlining.
///
/// Set to [`InlineMode::Block`] to ensure the widget layouts as a block item if the parent
/// is inlining.
///
/// Note that even if set to [`InlineMode::Inline`] the widget will only inline if all properties support
/// inlining.
#[property(WIDGET, default(InlineMode::Allow))]
pub fn inline(child: impl UiNode, mode: impl IntoVar<InlineMode>) -> impl UiNode {
let mode = mode.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_layout(&mode);
}
UiNodeOp::Measure { wm, desired_size } => {
*desired_size = match mode.get() {
InlineMode::Allow => child.measure(wm),
InlineMode::Inline => {
if LAYOUT.inline_constraints().is_none() {
// enable inline for content.
wm.with_inline_visual(|wm| child.measure(wm))
} else {
// already enabled by parent
child.measure(wm)
}
}
InlineMode::Block => {
// disable inline, method also disables in `WidgetMeasure`
wm.measure_block(child)
}
};
}
UiNodeOp::Layout { wl, final_size } => {
*final_size = match mode.get() {
InlineMode::Allow => child.layout(wl),
InlineMode::Inline => {
if LAYOUT.inline_constraints().is_none() {
wl.to_measure(None).with_inline_visual(|wm| child.measure(wm));
wl.with_inline_visual(|wl| child.layout(wl))
} else {
// already enabled by parent
child.layout(wl)
}
}
InlineMode::Block => {
if wl.inline().is_some() {
tracing::error!("inline enabled in `layout` when it signaled disabled in the previous `measure`");
wl.layout_block(child)
} else {
child.layout(wl)
}
}
};
}
_ => {}
})
}

108
zero-ui-wgt/src/lib.rs Normal file
View File

@ -0,0 +1,108 @@
//! Basic widget properties and helpers for declaring widgets and properties.
// suppress nag about very simple boxed closure signatures.
#![allow(clippy::type_complexity)]
#[doc(hidden)]
pub use zero_ui_app as __proc_macro_util;
/// Prelude for declaring properties and widgets.
pub mod prelude {
pub use zero_ui_app::{
event::{
command, event, event_args, AnyEventArgs as _, Command, CommandHandle, CommandInfoExt as _, CommandNameExt as _, Event,
EventArgs as _, EventHandle, EventHandles, EventPropagationHandle,
},
handler::{app_hn, app_hn_once, async_app_hn, async_app_hn_once, async_hn, async_hn_once, hn, hn_once, AppHandler, WidgetHandler},
render::{FrameBuilder, FrameUpdate, FrameValue, FrameValueKey, FrameValueUpdate, SpatialFrameId, TransformStyle},
shortcut::{shortcut, CommandShortcutExt as _, Shortcut, ShortcutFilter, Shortcuts},
timer::{DeadlineVar, TimerVar, TIMERS},
update::{EventUpdate, UpdateOp, WidgetUpdates, UPDATES},
widget::{
base::{WidgetBase, WidgetImpl},
border::{BorderSides, BorderStyle, CornerRadius, CornerRadiusFit, LineOrientation, LineStyle, BORDER},
builder::{property_id, NestGroup, WidgetBuilder},
easing,
info::{
InteractionPath, Interactivity, Visibility, WidgetBorderInfo, WidgetBoundsInfo, WidgetInfo, WidgetInfoBuilder,
WidgetLayout, WidgetMeasure, WidgetPath,
},
instance::{
match_node, match_node_leaf, match_node_list, match_node_typed, match_widget, ui_vec, ArcNode, ArcNodeList, BoxedUiNode,
BoxedUiNodeList, FillUiNode, NilUiNode, PanelList, SortingList, UiNode, UiNodeList, UiNodeListObserver, UiNodeOp,
UiNodeVec, ZIndex,
},
property, ui_node, widget, widget_impl, widget_mixin, widget_set, AnyVarSubscribe as _, VarLayout as _, VarSubscribe as _,
WidgetId, WidgetUpdateMode, WIDGET,
},
window::{MonitorId, WindowId, WINDOW},
};
pub use zero_ui_var::{
context_var, expr_var, impl_from_and_into_var, merge_var, response_done_var, response_var, state_var, var, when_var, AnyVar as _,
ArcVar, BoxedVar, ContextVar, IntoValue, IntoVar, LocalVar, ReadOnlyArcVar, ResponderVar, ResponseVar, Var, VarCapabilities,
VarHandle, VarHandles, VarValue,
};
pub use zero_ui_layout::{
context::{LayoutDirection, LayoutMetrics, DIRECTION_VAR, LAYOUT},
units::{
Align, AngleDegree, AngleGradian, AngleRadian, AngleUnits as _, ByteUnits as _, Dip, DipBox, DipPoint, DipRect, DipSideOffsets,
DipSize, DipToPx as _, DipVector, Factor, FactorPercent, FactorSideOffsets, FactorUnits as _, Layout1d as _, Layout2d as _,
LayoutAxis, Length, LengthUnits as _, Line, LineFromTuplesBuilder as _, Point, Px, PxBox, PxConstraints, PxConstraints2d,
PxCornerRadius, PxLine, PxPoint, PxRect, PxSideOffsets, PxSize, PxToDip as _, PxTransform, PxVector, Rect,
RectFromTuplesBuilder as _, ResolutionUnits as _, SideOffsets, Size, TimeUnits as _, Transform, Vector,
},
};
pub use zero_ui_txt::{formatx, ToText as _, Txt};
pub use zero_ui_clone_move::{async_clmv, async_clmv_fn, async_clmv_fn_once, clmv};
pub use zero_ui_task as task;
pub use zero_ui_app_context::{
app_local, context_local, CaptureFilter, ContextLocal, ContextValueSet, FullLocalContext, LocalContext, RunOnDrop,
};
pub use zero_ui_state_map::{state_map, OwnedStateMap, StateId, StateMapMut, StateMapRef, StaticStateId};
pub use zero_ui_unique_id::{IdEntry, IdMap, IdSet};
pub use zero_ui_color::{
color_scheme_highlight, color_scheme_map, color_scheme_pair, colors, filters as color_filters, gradient, hex, hsl, hsla, hsv, hsva,
rgb, rgba, web_colors, ColorPair, ColorScheme, Hsla, Hsva, MixBlendMode, Rgba,
};
pub use crate::nodes::{
bind_is_state, border_node, command_property, event_is_state, event_is_state2, event_is_state3, event_is_state4, event_property,
fill_node, widget_state_get_state, widget_state_is_state, with_context_blend, with_context_local, with_context_local_init,
with_context_var, with_context_var_init, with_widget_state, with_widget_state_modify,
};
}
pub mod nodes;
mod border_props;
mod clip_props;
mod color_props;
mod hit_test_props;
mod interactivity_props;
mod layout_props;
mod node_events;
mod panel_props;
mod parallel_prop;
mod visibility_props;
mod wgt;
pub use border_props::*;
pub use clip_props::*;
pub use color_props::*;
pub use hit_test_props::*;
pub use interactivity_props::*;
pub use layout_props::*;
pub use node_events::*;
pub use panel_props::*;
pub use parallel_prop::*;
pub use visibility_props::*;
pub use wgt::*;

View File

@ -0,0 +1,348 @@
use std::time::Instant;
use zero_ui_app::widget::instance::UiNodeOpMethod;
use crate::prelude::*;
/// Widget UI node events.
#[widget_mixin]
pub struct WidgetEventMix<P>(P);
/// Represents a node operation.
#[derive(Clone, Debug)]
pub struct OnNodeOpArgs {
/// Operation.
///
/// Event args must be static so access to the full [`UiNodeOp`] is not possible, you can quickly
/// declare a new property with [`property`] and [`match_node`] if you want to affect the widget this way.
pub op: UiNodeOpMethod,
/// Number of times the handler was called.
///
/// The number is `1` for the first call and is not reset if the widget is re-inited.
pub count: usize,
/// Instant the handler was called.
pub timestamp: Instant,
}
impl OnNodeOpArgs {
/// New args.
pub fn new(op: UiNodeOpMethod, count: usize, timestamp: Instant) -> Self {
Self { op, count, timestamp }
}
/// New args with timestamp now.
pub fn now(op: UiNodeOpMethod, count: usize) -> Self {
Self::new(op, count, Instant::now())
}
}
/// On any node operation.
///
/// This property calls `handler` for any widget node operation, after the widget content has processed the operation. This means
/// that the `handler` is raised after any [`on_pre_node_op`] handler. Note that properties of [`NestGroup::EVENT`] or lower
/// can still process the operation before this event.
///
/// # Handlers
///
/// This property accepts any [`WidgetHandler`], including the async handlers. Use one of the handler macros, [`hn!`],
/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers spawn a task that is associated with the widget, it will only update when the widget updates,
/// so the task *pauses* when the widget is deinited, and is *canceled* when the widget is dropped.
///
/// [`on_pre_node_op`]: fn@on_pre_node_op
/// [`NestGroup::EVENT`]: zero_ui_app::widget::builder::NestGroup::EVENT
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_node_op(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_node_op_impl(child, handler, |_| true)
}
fn on_node_op_impl(
child: impl UiNode,
handler: impl WidgetHandler<OnNodeOpArgs>,
filter: impl Fn(UiNodeOpMethod) -> bool + Send + 'static,
) -> impl UiNode {
let mut handler = handler.cfg_boxed();
let mut count = 1;
match_node(child, move |child, op| {
let mtd = op.mtd();
child.op(op);
if filter(mtd) {
handler.event(&OnNodeOpArgs::now(mtd, count));
count = count.wrapping_add(1);
}
if let UiNodeOpMethod::Update = mtd {
handler.update();
}
})
}
/// Preview [`on_node_op`] event.
///
/// This property calls `handler` for any widget node operation, before most of the widget content processes the operation. This means
/// that the `handler` is raised before any [`on_node_op`] handler. Note that properties of [`NestGroup::EVENT`] or lower
/// can still process the operation before this event.
///
/// # Handlers
///
/// This property accepts any [`WidgetHandler`], including the async handlers. Use one of the handler macros, [`hn!`],
/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers spawn a task that is associated with the widget, it will only update when the widget updates,
/// so the task *pauses* when the widget is deinited, and is *canceled* when the widget is dropped.
///
/// [`on_node_op`]: fn@on_node_op
/// [`NestGroup::EVENT`]: zero_ui_app::widget::builder::NestGroup::EVENT
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_pre_node_op(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_pre_node_op_impl(child, handler, |_| true)
}
fn on_pre_node_op_impl(
child: impl UiNode,
handler: impl WidgetHandler<OnNodeOpArgs>,
filter: impl Fn(UiNodeOpMethod) -> bool + Send + 'static,
) -> impl UiNode {
let mut handler = handler.cfg_boxed();
let mut count = 1;
match_node(child, move |_, op| {
if let UiNodeOp::Update { .. } = &op {
handler.update();
}
let mtd = op.mtd();
if filter(mtd) {
handler.event(&OnNodeOpArgs::now(mtd, count));
count = count.wrapping_add(1);
}
})
}
/// Widget [`init`](UiNode::init) event.
///
/// This property calls `handler` when the widget and its content initializes. Note that widgets
/// can be [deinitialized](fn@on_deinit) and reinitialized, so the `handler` can be called more then once,
/// you can use one of the *once* handlers to only be called once or use the arguments [`count`](OnNodeOpArgs::count).
/// to determinate if you are in the first init.
///
/// Note that the widget is not in the [`WidgetInfoTree`] when this event happens, you can use [`on_info_init`] for initialization
/// that depends on the widget info.
///
/// # Handlers
///
/// This property accepts any [`WidgetHandler`], including the async handlers. Use one of the handler macros, [`hn!`],
/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers spawn a task that is associated with the widget, it will only update when the widget updates,
/// so the task *pauses* when the widget is deinited, and is *canceled* when the widget is dropped.
///
/// [`on_info_init`]: fn@on_info_init
/// [`WidgetInfoTree`]: zero_ui_app::widget::info::WidgetInfoTree
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_init(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_node_op_impl(child, handler, |op| matches!(op, UiNodeOpMethod::Init))
}
/// Preview [`on_init`] event.
///
/// This property calls `handler` when the widget initializes, before the widget content initializes. This means
/// that the `handler` is raised before any [`on_init`] handler.
///
/// # Handlers
///
/// This property accepts any [`WidgetHandler`], including the async handlers. Use one of the handler macros, [`hn!`],
/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers spawn a task that is associated with the widget, it will only update when the widget updates,
/// so the task *pauses* when the widget is deinited, and is *canceled* when the widget is dropped.
///
/// [`on_init`]: fn@on_init
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_pre_init(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_pre_node_op_impl(child, handler, |op| matches!(op, UiNodeOpMethod::Init))
}
/// Widget inited and info collected event.
///
/// This event fires after the first [`UiNode::info`] construction after [`UiNode::init`]. This event can be used when
/// some widget initialization needs to happen, but the widget must be in the [`WidgetInfoTree`] for it to work.
///
/// # Handlers
///
/// This property accepts any [`WidgetHandler`], including the async handlers. Use one of the handler macros, [`hn!`],
/// [`hn_once!`], [`async_hn!`] or [`async_hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers spawn a task that is associated with the widget, it will only update when the widget updates,
/// so the task *pauses* when the widget is deinited, and is *canceled* when the widget is dropped.
///
/// [`WidgetInfoTree`]: zero_ui_app::widget::info::WidgetInfoTree
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_info_init(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
let mut handler = handler.cfg_boxed();
let mut count = 1;
enum State {
WaitInfo,
InfoInited,
Done,
}
let mut state = State::WaitInfo;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
state = State::WaitInfo;
}
UiNodeOp::Info { .. } => {
if let State::WaitInfo = &state {
state = State::InfoInited;
WIDGET.update();
}
}
UiNodeOp::Update { updates } => {
child.update(updates);
if let State::InfoInited = &state {
state = State::Done;
handler.event(&OnNodeOpArgs::now(UiNodeOpMethod::Update, count));
count = count.wrapping_add(1);
}
handler.update();
}
_ => {}
})
}
/// Widget [`update`](UiNode::update) event.
///
/// This property calls `handler` every UI update, after the widget content updates. Updates happen in
/// high volume in between idle moments, so the handler code should be considered a *hot-path*.
///
/// # Handlers
///
/// You can use one of the handler macros, [`hn!`] or [`hn_once!`], to declare a handler closure. You must avoid using the async
/// handlers as they cause an update every time the UI task advances from an await point causing another task to spawn.
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_update(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_node_op_impl(child, handler, |op| matches!(op, UiNodeOpMethod::Update))
}
/// Preview [`on_update`] event.
///
/// This property calls `handler` every time the UI updates, before the widget content updates. This means
/// that the `handler` is raised before any [`on_init`] handler.
///
/// # Handlers
///
/// You can use one of the handler macros, [`hn!`] or [`hn_once!`], to declare a handler closure. You must avoid using the async
/// handlers as they cause an update every time the UI task advances from an await point causing another task to spawn.
///
/// [`on_update`]: fn@on_update
/// [`on_init`]: fn@on_init
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_pre_update(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_pre_node_op_impl(child, handler, |op| matches!(op, UiNodeOpMethod::Update))
}
/// Arguments for the [`on_deinit`](fn@on_deinit) event.
#[derive(Clone, Debug, Copy)]
pub struct OnDeinitArgs {
/// Number of time the handler was called.
///
/// The number is `1` for the first call.
pub count: usize,
}
/// Widget [`deinit`](UiNode::deinit) event.
///
/// This property calls `handler` when the widget deinits, after the widget content deinits. Note that
/// widgets can be [reinitialized](fn@on_init) so the `handler` can be called more then once,
/// you can use one of the *once* handlers to only be called once or use the arguments [`count`](OnDeinitArgs::count)
/// to determinate if you are in the first deinit.
///
/// # Handlers
///
/// This property accepts the [`WidgetHandler`] that are not async. Use one of the handler macros, [`hn!`] or
/// [`hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers do not work here because widget bound async tasks only advance past the first `.await`
/// during widget updates, but we are deiniting the widget, probably about to drop it. You can start an UI bound
/// async task in the app context using [`UPDATES.run`] or you can use [`task::spawn`] to start a parallel async task
/// in a worker thread.
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_deinit(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_node_op_impl(child, handler, |op| matches!(op, UiNodeOpMethod::Deinit))
}
/// Preview [`on_update`] event.
///
/// This property calls `handler` every time the UI updates, before the widget content updates. This means
/// that the `handler` is raised before any [`on_init`] handler.
///
/// # Handlers
///
/// This property accepts the [`WidgetHandler`] that are not async. Use one of the handler macros, [`hn!`] or
/// [`hn_once!`], to declare a handler closure.
///
/// ## Async
///
/// The async handlers do not work here because widget bound async tasks only advance past the first `.await`
/// during widget updates, but we are deiniting the widget, probably about to drop it. You can start an UI bound
/// async task in the app context using [`UPDATES.run`] or you can use [`task::spawn`] to start a parallel async task
/// in a worker thread.
///
/// [`on_update`]: fn@on_update
/// [`on_init`]: fn@on_init
#[property(EVENT, widget_impl(WidgetEventMix<P>))]
pub fn on_pre_deinit(child: impl UiNode, handler: impl WidgetHandler<OnNodeOpArgs>) -> impl UiNode {
on_pre_node_op_impl(child, handler, |op| matches!(op, UiNodeOpMethod::Deinit))
}
/// If the widget has inited.
///
/// The `state` is set to `true` on init and to `false` on deinit. This property is useful for
/// declaring transition animations that play on init using `when` blocks.
///
/// # Examples
///
/// Animate a popup when it opens:
///
/// ```
/// use zero_ui::prelude::*;
///
/// # let _ =
/// popup::Popup! {
/// opacity = 0.pct();
/// y = -10;
/// when *#is_inited {
/// #[easing(100.ms())]
/// opacity = 100.pct();
/// #[easing(100.ms())]
/// y = 0;
/// }
///
/// // ..
/// }
/// # ;
/// ```
#[property(CONTEXT)]
pub fn is_inited(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
let state = state.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
let _ = state.set(true);
}
UiNodeOp::Deinit => {
let _ = state.set(false);
}
_ => {}
})
}

View File

@ -10,8 +10,9 @@ use zero_ui_app::{
render::{FrameBuilder, FrameValueKey},
widget::{
border::{BORDER, BORDER_ALIGN_VAR, BORDER_OVER_VAR},
info::Interactivity,
instance::*,
VarLayout, WIDGET,
VarLayout, WidgetUpdateMode, WIDGET,
},
};
use zero_ui_app_context::{ContextLocal, LocalContext};
@ -44,7 +45,7 @@ pub use zero_ui_app;
/// # fn main() -> () { }
/// # use zero_ui_app::{*, widget::{instance::*, *}};
/// # use zero_ui_var::*;
/// # use zero_ui_widget::nodes::*;
/// # use zero_ui_wgt::nodes::*;
/// #
/// context_var! {
/// pub static FOO_VAR: u32 = 0u32;
@ -70,7 +71,7 @@ pub use zero_ui_app;
/// # fn main() -> () { }
/// # use zero_ui_app::{*, widget::{instance::*, *}};
/// # use zero_ui_var::*;
/// # use zero_ui_widget::nodes::*;
/// # use zero_ui_wgt::nodes::*;
/// #
/// #[derive(Debug, Clone, Default, PartialEq)]
/// pub struct Config {
@ -174,116 +175,6 @@ pub fn with_context_var_init<T: VarValue>(
})
}
#[doc(hidden)]
#[macro_export]
macro_rules! __event_property {
(
$(#[$on_event_attrs:meta])*
$vis:vis fn $event:ident {
event: $EVENT:path,
args: $Args:path,
filter: $filter:expr,
with: $($with:expr)? $(,)?
}
) => { $crate::nodes::paste! {
$(#[$on_event_attrs])*
///
/// # Preview
///
#[doc = "You can preview this event using [`on_pre_"$event "`](fn.on_pre_"$event ".html)."]
/// Otherwise the handler is only called after the widget content has a chance of handling the event by stopping propagation.
///
/// # Async
///
/// You can use async event handlers with this property.
#[$crate::nodes::zero_ui_app::widget::property(EVENT, default( $crate::nodes::zero_ui_app::handler::hn!(|_|{}) ))]
$vis fn [<on_ $event>](
child: impl $crate::nodes::zero_ui_app::widget::instance::UiNode,
handler: impl $crate::nodes::zero_ui_app::handler::WidgetHandler<$Args>,
) -> impl $crate::nodes::zero_ui_app::widget::instance::UiNode {
$crate::__event_property!(with($crate::nodes::on_event(child, $EVENT, $filter, handler), false, $($with)?))
}
#[doc = "Preview [`on_"$event "`](fn.on_"$event ".html) event."]
///
/// # Preview
///
/// Preview event properties call the handler before the main event property and before the widget content, if you stop
/// the propagation of a preview event the main event handler is not called.
///
/// # Async
///
/// You can use async event handlers with this property, note that only the code before the fist `.await` is *preview*,
/// subsequent code runs in widget updates.
#[$crate::nodes::zero_ui_app::widget::property(EVENT, default( $crate::nodes::zero_ui_app::handler::hn!(|_|{}) ))]
$vis fn [<on_pre_ $event>](
child: impl $crate::nodes::zero_ui_app::widget::instance::UiNode,
handler: impl $crate::nodes::zero_ui_app::handler::WidgetHandler<$Args>,
) -> impl $crate::nodes::zero_ui_app::widget::instance::UiNode {
$crate::__event_property!(with($crate::nodes::on_pre_event(child, $EVENT, $filter, handler), true, $($with)?))
}
} };
(
$(#[$on_event_attrs:meta])*
$vis:vis fn $event:ident {
event: $EVENT:path,
args: $Args:path,
}
) => {
$crate::__event_property! {
$(#[$on_event_attrs])*
$vis fn $event {
event: $EVENT,
args: $Args,
filter: |_args| true,
with:
}
}
};
(
$(#[$on_event_attrs:meta])*
$vis:vis fn $event:ident {
event: $EVENT:path,
args: $Args:path,
filter: $filter:expr,
}
) => {
$crate::__event_property! {
$(#[$on_event_attrs])*
$vis fn $event {
event: $EVENT,
args: $Args,
filter: $filter,
with:
}
}
};
(
$(#[$on_event_attrs:meta])*
$vis:vis fn $event:ident {
event: $EVENT:path,
args: $Args:path,
with: $with:expr,
}
) => {
$crate::__event_property! {
$(#[$on_event_attrs])*
$vis fn $event {
event: $EVENT,
args: $Args,
filter: |_args| true,
with: $with,
}
}
};
(with($child:expr, $preview:expr,)) => { $child };
(with($child:expr, $preview:expr, $with:expr)) => { ($with)($child, $preview) };
}
///<span data-del-macro-root></span> Declare one or more event properties.
///
/// Each declaration expands to two properties `on_$event`, `on_pre_$event`.
@ -294,7 +185,7 @@ macro_rules! __event_property {
/// ```
/// # fn main() { }
/// # use zero_ui_app::event::*;
/// # use zero_ui_widget::nodes::*;
/// # use zero_ui_wgt::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; }
@ -337,6 +228,30 @@ macro_rules! __event_property {
///
/// You can use [`command_property`] to declare command event properties.
///
/// # Implement For
///
/// You can implement the new properties for a widget or mix-in using `widget_impl`:
///
/// ```
/// # fn main() { }
/// # use zero_ui_app::{event::*, widget::{instance::UiNode, widget_mixin}};
/// # use zero_ui_wgt::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 }
/// /// Keyboard events.
/// #[widget_mixin]
/// pub struct KeyboardMix<P>(P);
///
/// event_property! {
/// pub fn key_input {
/// event: KEY_INPUT_EVENT,
/// args: KeyInputArgs,
/// widget_impl: KeyboardMix<P>,
/// }
/// }
/// ```
///
/// # With Extra Nodes
///
/// You can wrap the event handler node with extra nodes by setting the optional `with` closure:
@ -344,7 +259,7 @@ macro_rules! __event_property {
/// ```
/// # fn main() { }
/// # use zero_ui_app::{event::*, widget::instance::UiNode};
/// # use zero_ui_widget::nodes::*;
/// # use zero_ui_wgt::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 }
@ -372,23 +287,231 @@ macro_rules! event_property {
$(#[$on_event_attrs:meta])*
$vis:vis fn $event:ident {
event: $EVENT:path,
args: $Args:path $(,
filter: $filter:expr)? $(,
with: $with:expr)? $(,)?
args: $Args:path,
$(filter: $filter:expr,)?
$(widget_impl: $Wgt:ty,)?
$(with: $with:expr,)?
}
)+) => {$(
$crate::__event_property! {
$(#[$on_event_attrs])*
$vis fn $event {
event: $EVENT,
args: $Args,
$(filter: $filter,)?
$(with: $with,)?
done {
sig { $(#[$on_event_attrs])* $vis fn $event { event: $EVENT, args: $Args, } }
}
$(filter: $filter,)?
$(widget_impl: $Wgt,)?
$(with: $with,)?
}
)+};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __event_property {
// match filter:
(
done {
$($done:tt)+
}
filter: $filter:expr,
$($rest:tt)*
) => {
$crate::__event_property! {
done {
$($done)+
filter { $filter }
}
$($rest)*
}
};
// match widget_impl:
(
done {
$($done:tt)+
}
widget_impl: $Wgt:ty,
$($rest:tt)*
) => {
$crate::__event_property! {
done {
$($done)+
widget_impl { , widget_impl($Wgt) }
}
$($rest)*
}
};
// match with:
(
done {
$($done:tt)+
}
with: $with:expr,
) => {
$crate::__event_property! {
done {
$($done)+
with { $with }
}
}
};
// match done sig
(
done {
sig { $($sig:tt)+ }
}
) => {
$crate::__event_property! {
done {
sig { $($sig)+ }
filter { |_args| true }
widget_impl { }
with { }
}
}
};
// match done sig+filter
(
done {
sig { $($sig:tt)+ }
filter { $($filter:tt)+ }
}
) => {
$crate::__event_property! {
done {
sig { $($sig)+ }
filter { $($filter)+ }
widget_impl { }
with { }
}
}
};
// match done sig+widget_impl
(
done {
sig { $($sig:tt)+ }
widget_impl { $($widget_impl:tt)+ }
}
) => {
$crate::__event_property! {
done {
sig { $($sig)+ }
filter { |_args| true }
widget_impl { $($widget_impl)+ }
with { }
}
}
};
// match done sig+with
(
done {
sig { $($sig:tt)+ }
with { $($with:tt)+ }
}
) => {
$crate::__event_property! {
done {
sig { $($sig)+ }
filter { |_args| true }
widget_impl { }
with { $($with)+ }
}
}
};
// match done sig+filter+widget_impl
(
done {
sig { $($sig:tt)+ }
filter { $($filter:tt)+ }
widget_impl { $($widget_impl:tt)+ }
}
) => {
$crate::__event_property! {
done {
sig { $($sig)+ }
filter { $($filter)+ }
widget_impl { $($widget_impl)+ }
with { }
}
}
};
// match done sig+filter+with
(
done {
sig { $($sig:tt)+ }
filter { $($filter:tt)+ }
with { $($with:tt)+ }
}
) => {
$crate::__event_property! {
done {
sig { $($sig)+ }
filter { $($filter)+ }
widget_impl { }
with { $($with)+ }
}
}
};
// match done sig+filter+widget_impl+with
(
done {
sig { $(#[$on_event_attrs:meta])* $vis:vis fn $event:ident { event: $EVENT:path, args: $Args:path, } }
filter { $filter:expr }
widget_impl { $($widget_impl:tt)* }
with { $($with:expr)? }
}
) => {
$crate::nodes::paste! {
$(#[$on_event_attrs])*
///
/// # Preview
///
#[doc = "You can preview this event using [`on_pre_"$event "`](fn.on_pre_"$event ".html)."]
/// Otherwise the handler is only called after the widget content has a chance of handling the event by stopping propagation.
///
/// # Async
///
/// You can use async event handlers with this property.
#[$crate::nodes::zero_ui_app::widget::property(
EVENT,
default( $crate::nodes::zero_ui_app::handler::hn!(|_|{}) )
$($widget_impl)*
)]
$vis fn [<on_ $event>](
child: impl $crate::nodes::zero_ui_app::widget::instance::UiNode,
handler: impl $crate::nodes::zero_ui_app::handler::WidgetHandler<$Args>,
) -> impl $crate::nodes::zero_ui_app::widget::instance::UiNode {
$crate::__event_property!(=> with($crate::nodes::on_event(child, $EVENT, $filter, handler), false, $($with)?))
}
#[doc = "Preview [`on_"$event "`](fn.on_"$event ".html) event."]
///
/// # Preview
///
/// Preview event properties call the handler before the main event property and before the widget content, if you stop
/// the propagation of a preview event the main event handler is not called.
///
/// # Async
///
/// You can use async event handlers with this property, note that only the code before the fist `.await` is *preview*,
/// subsequent code runs in widget updates.
#[$crate::nodes::zero_ui_app::widget::property(
EVENT,
default( $crate::nodes::zero_ui_app::handler::hn!(|_|{}) )
$($widget_impl)*
)]
$vis fn [<on_pre_ $event>](
child: impl $crate::nodes::zero_ui_app::widget::instance::UiNode,
handler: impl $crate::nodes::zero_ui_app::handler::WidgetHandler<$Args>,
) -> impl $crate::nodes::zero_ui_app::widget::instance::UiNode {
$crate::__event_property!(=> with($crate::nodes::on_pre_event(child, $EVENT, $filter, handler), true, $($with)?))
}
}
};
(=> with($child:expr, $preview:expr,)) => { $child };
(=> with($child:expr, $preview:expr, $with:expr)) => { ($with)($child, $preview) };
}
/// Helper for declaring event properties.
///
/// This function is used by the [`event_property!`] macro.
@ -597,7 +720,7 @@ macro_rules! __command_property {
/// # fn main() { }
/// # use zero_ui_app::{event::*, widget::*};
/// # use zero_ui_app::var::*;
/// # use zero_ui_widget::nodes::*;
/// # use zero_ui_wgt::nodes::*;
/// # command! {
/// # pub static PASTE_CMD;
/// # }
@ -1477,7 +1600,7 @@ fn with_context_local_init_impl<T: Any + Send + Sync + 'static>(
///
/// Panics during init if `ctx` is not from the same app as the init context.
///
/// [`NestGroup::CHILD`]: crate::widget_builder::NestGroup::CHILD
/// [`NestGroup::CHILD`]: zero_ui_app::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 {
@ -1501,7 +1624,10 @@ pub fn with_context_blend(mut ctx: LocalContext, over: bool, child: impl UiNode)
///
/// ```
/// # fn main() -> () { }
/// use zero_ui_core::{property, context::*, var::IntoVar, widget_instance::UiNode};
/// use zero_ui_app::{widget::{property, instance::UiNode, WIDGET, WidgetUpdateMode}};
/// use zero_ui_var::IntoVar;
/// use zero_ui_wgt::nodes::with_widget_state;
/// use zero_ui_state_map::{StaticStateId, StateId};
///
/// pub static FOO_ID: StaticStateId<u32> = StateId::new_static();
///
@ -1635,6 +1761,66 @@ where
})
}
/// Create a node that disables interaction for all widget inside `node` using [`BLOCKED`].
///
/// Unlike the `interactive` property this does not apply to the contextual widget, only `child` and descendants.
///
/// The node works for both if the `child` is a widget or if it contains widgets, the performance
/// is slightly better if the `child` is a widget directly.
///
/// [`BLOCKED`]: Interactivity::BLOCKED
pub fn interactive_node(child: impl UiNode, interactive: impl IntoVar<bool>) -> impl UiNode {
let interactive = interactive.into_var();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&interactive);
}
UiNodeOp::Info { info } => {
if interactive.get() {
child.info(info);
} else if let Some(id) = child.with_context(WidgetUpdateMode::Ignore, || WIDGET.id()) {
// child is a widget.
info.push_interactivity_filter(move |args| {
if args.info.id() == id {
Interactivity::BLOCKED
} else {
Interactivity::ENABLED
}
});
child.info(info);
} else {
let block_range = info.with_children_range(|info| child.info(info));
if !block_range.is_empty() {
// has child widgets.
let id = WIDGET.id();
info.push_interactivity_filter(move |args| {
if let Some(parent) = args.info.parent() {
if parent.id() == id {
// check child range
for (i, item) in parent.children().enumerate() {
if item == args.info {
return if !block_range.contains(&i) {
Interactivity::ENABLED
} else {
Interactivity::BLOCKED
};
} else if i >= block_range.end {
break;
}
}
}
}
Interactivity::ENABLED
});
}
}
}
_ => {}
})
}
#[doc(inline)]
pub use crate::command_property;
#[doc(inline)]

View File

@ -0,0 +1,41 @@
use zero_ui_app::widget::instance::Z_INDEX;
use crate::prelude::*;
/// Defines the render order of a widget in a layout panel.
///
/// When set the widget will still update and layout according to their *logical* position in the list but
/// they will render according to the order defined by the [`ZIndex`] value.
///
/// Layout panels that support this property should mention it in their documentation, implementers
/// see [`PanelList`] for more details.
///
/// An error is logged on init if the widget is not a direct child of a Z-sorting panel.
#[property(CONTEXT, default(ZIndex::DEFAULT))]
pub fn z_index(child: impl UiNode, index: impl IntoVar<ZIndex>) -> impl UiNode {
let index = index.into_var();
let mut valid = false;
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
valid = Z_INDEX.set(index.get());
if valid {
WIDGET.sub_var(&index);
} else {
tracing::error!(
"property `z_index` set for `{}` but it is not the direct child of a Z-sorting panel",
WIDGET.trace_id()
);
}
}
UiNodeOp::Update { .. } => {
if valid {
if let Some(i) = index.get_new() {
assert!(Z_INDEX.set(i));
}
}
}
_ => {}
})
}

View File

@ -0,0 +1,15 @@
use zero_ui_app::widget::base::{Parallel, PARALLEL_VAR};
use crate::prelude::*;
/// Defines what node list methods can run in parallel in the widget and descendants.
///
/// This property sets the [`PARALLEL_VAR`] that is used by [`UiNodeList`] implementers to toggle parallel processing.
///
/// See also `WINDOWS.parallel` to define parallelization in multi-window apps.
///
/// [`UiNode`]: zero_ui_app::widget::instance::UiNodeList
#[property(CONTEXT, default(PARALLEL_VAR))]
pub fn parallel(child: impl UiNode, enabled: impl IntoVar<Parallel>) -> impl UiNode {
with_context_var(child, PARALLEL_VAR, enabled)
}

View File

@ -0,0 +1,203 @@
use crate::prelude::*;
use zero_ui_app::widget::info;
/// Visibility properties.
///
/// Mixin defines visibility and visibility state probing properties for all widgets.
#[widget_mixin]
pub struct VisibilityMix<P>(P);
/// Sets the widget visibility.
///
/// This property causes the widget to have the `visibility`, the widget actual visibility is computed, for example,
/// widgets that don't render anything are considered `Hidden` even if the visibility property is not set, this property
/// only forces the widget to layout and render according to the specified visibility.
///
/// To probe the visibility state of a widget in `when` clauses use [`is_visible`], [`is_hidden`] or [`is_collapsed`] in `when` clauses,
/// to probe a widget state use [`UiNode::with_context`] or [`WidgetInfo::visibility`].
///
/// # Implicit
///
/// This property is included in all widgets by default, you don't need to import it to use it.
///
/// [`is_visible`]: fn@is_visible
/// [`is_hidden`]: fn@is_hidden
/// [`is_collapsed`]: fn@is_collapsed
/// [`WidgetInfo::visibility`]: zero_ui_app::widget::info::WidgetInfo::visibility
#[property(CONTEXT, default(true), widget_impl(VisibilityMix<P>))]
pub fn visibility(child: impl UiNode, visibility: impl IntoVar<Visibility>) -> impl UiNode {
let visibility = visibility.into_var();
let mut prev_vis = Visibility::Visible;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&visibility);
prev_vis = visibility.get();
}
UiNodeOp::Update { .. } => {
if let Some(vis) = visibility.get_new() {
use Visibility::*;
match (prev_vis, vis) {
(Collapsed, Visible) | (Visible, Collapsed) => {
WIDGET.layout().render();
}
(Hidden, Visible) | (Visible, Hidden) => {
WIDGET.render();
}
(Collapsed, Hidden) | (Hidden, Collapsed) => {
WIDGET.layout();
}
_ => {}
}
prev_vis = vis;
}
}
UiNodeOp::Measure { wm, desired_size } => {
if Visibility::Collapsed != visibility.get() {
*desired_size = child.measure(wm);
} else {
child.delegated();
}
}
UiNodeOp::Layout { wl, final_size } => {
if Visibility::Collapsed != visibility.get() {
*final_size = child.layout(wl);
} else {
wl.collapse();
child.delegated();
}
}
UiNodeOp::Render { frame } => match visibility.get() {
Visibility::Visible => child.render(frame),
Visibility::Hidden => frame.hide(|frame| child.render(frame)),
Visibility::Collapsed => {
child.delegated();
#[cfg(debug_assertions)]
{
tracing::error!(
"collapsed {} rendered, to fix, layout the widget, or `WidgetLayout::collapse_child` the widget",
WIDGET.trace_id()
)
}
}
},
UiNodeOp::RenderUpdate { update } => match visibility.get() {
Visibility::Visible => child.render_update(update),
Visibility::Hidden => update.hidden(|update| child.render_update(update)),
Visibility::Collapsed => {
child.delegated();
#[cfg(debug_assertions)]
{
tracing::error!(
"collapsed {} render-updated, to fix, layout the widget, or `WidgetLayout::collapse_child` the widget",
WIDGET.trace_id()
)
}
}
},
_ => {}
})
}
fn visibility_eq_state(child: impl UiNode, state: impl IntoVar<bool>, expected: Visibility) -> impl UiNode {
event_is_state(
child,
state,
expected == Visibility::Visible,
info::VISIBILITY_CHANGED_EVENT,
move |_| {
let tree = WINDOW.info();
let vis = tree.get(WIDGET.id()).map(|w| w.visibility()).unwrap_or(Visibility::Visible);
Some(vis == expected)
},
)
}
/// If the widget is [`Visible`](Visibility::Visible).
#[property(CONTEXT, widget_impl(VisibilityMix<P>))]
pub fn is_visible(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
visibility_eq_state(child, state, Visibility::Visible)
}
/// If the widget is [`Hidden`](Visibility::Hidden).
#[property(CONTEXT, widget_impl(VisibilityMix<P>))]
pub fn is_hidden(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
visibility_eq_state(child, state, Visibility::Hidden)
}
/// If the widget is [`Collapsed`](Visibility::Collapsed).
#[property(CONTEXT, widget_impl(VisibilityMix<P>))]
pub fn is_collapsed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
visibility_eq_state(child, state, Visibility::Collapsed)
}
/// Defines if the widget only renders if it-s bounds intersects with the viewport auto-hide rectangle.
///
/// The auto-hide rect is usually *one viewport* of extra space around the viewport, so only widgets that transform
/// themselves very far need to set this, disabling auto-hide for an widget does not disable it for descendants.
///
/// # Examples
///
/// The example demonstrates a `container` that is *fixed* in the scroll viewport, it sets the `x` and `y` properties
/// to always stay in frame, but transforms set by a widget on itself always affects the [`inner_bounds`], the
/// [`outer_bounds`] will still be the transform set by the parent so the container may end-up auto-hidden.
///
/// Note that auto-hide is not disabled for the `content` widget, but it's [`outer_bounds`] is affected by the `container`
/// so it is auto-hidden correctly.
///
/// ```
/// # macro_rules! Container { ($($tt:tt)*) => { NilUiNode }}
/// # use zero_ui_app::widget::instance::*;
/// fn center_viewport(content: impl UiNode) -> impl UiNode {
/// Container! {
/// zero_ui::core::widget_base::can_auto_hide = false;
///
/// x = zero_ui::widgets::scroll::SCROLL_HORIZONTAL_OFFSET_VAR.map(|&fct| Length::Relative(fct) - 1.vw() * fct);
/// y = zero_ui::widgets::scroll::SCROLL_VERTICAL_OFFSET_VAR.map(|&fct| Length::Relative(fct) - 1.vh() * fct);
/// max_size = (1.vw(), 1.vh());
/// content_align = Align::CENTER;
///
/// content;
/// }
/// }
/// ```
///
/// [`outer_bounds`]: info::WidgetBoundsInfo::outer_bounds
/// [`inner_bounds`]: info::WidgetBoundsInfo::inner_bounds
#[property(CONTEXT, default(true), widget_impl(VisibilityMix<P>))]
pub fn can_auto_hide(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&enabled);
}
UiNodeOp::Update { .. } => {
if let Some(new) = enabled.get_new() {
if WIDGET.bounds().can_auto_hide() != new {
WIDGET.layout().render();
}
}
}
UiNodeOp::Layout { wl, .. } => {
wl.allow_auto_hide(enabled.get());
}
_ => {}
})
}
event_property! {
/// Widget global inner transform changed.
pub fn transform_changed {
event: info::TRANSFORM_CHANGED_EVENT,
args: info::TransformChangedArgs,
}
/// Widget global position changed.
pub fn move {
event: info::TRANSFORM_CHANGED_EVENT,
args: info::TransformChangedArgs,
filter: |a| a.offset() != PxVector::zero(),
}
}

8
zero-ui-wgt/src/wgt.rs Normal file
View File

@ -0,0 +1,8 @@
use crate::{prelude::*, HitTestMix, VisibilityMix, WidgetEventMix};
/// Minimal widget.
///
/// You can use this to create a quick new custom widget that is only used in one code place and can be created entirely
/// by properties and `when` conditions.
#[widget($crate::Wgt)]
pub struct Wgt(VisibilityMix<HitTestMix<WidgetEventMix<WidgetBase>>>);

View File

@ -1,583 +0,0 @@
//! 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::{
widget::{
base::{Parallel, PARALLEL_VAR},
border::{BORDER_ALIGN_VAR, BORDER_OVER_VAR, CORNER_RADIUS_FIT_VAR, CORNER_RADIUS_VAR},
info::{Interactivity, Visibility},
instance::*,
*,
},
window::WINDOW,
};
use zero_ui_clone_move::clmv;
use zero_ui_var::*;
pub mod nodes;
/// Minimal widget.
///
/// You can use this to create a quick new custom widget that is only used in one code place and can be created entirely
/// by properties and `when` conditions.
#[widget($crate::Wgt)]
pub struct Wgt(VisibilityMix<HitTestMix<base::WidgetBase>>);
/// Defines the render order of a widget in a layout panel.
///
/// When set the widget will still update and layout according to their *logical* position in the list but
/// they will render according to the order defined by the [`ZIndex`] value.
///
/// Layout panels that support this property should mention it in their documentation, implementers
/// see [`PanelList`] for more details.
///
/// An error is logged on init if the widget is not a direct child of a Z-sorting panel.
#[property(CONTEXT, default(ZIndex::DEFAULT))]
pub fn z_index(child: impl UiNode, index: impl IntoVar<ZIndex>) -> impl UiNode {
let index = index.into_var();
let mut valid = false;
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
valid = Z_INDEX.set(index.get());
if valid {
WIDGET.sub_var(&index);
} else {
tracing::error!(
"property `z_index` set for `{}` but it is not the direct child of a Z-sorting panel",
WIDGET.trace_id()
);
}
}
UiNodeOp::Update { .. } => {
if valid {
if let Some(i) = index.get_new() {
assert!(Z_INDEX.set(i));
}
}
}
_ => {}
})
}
/// Interactivity properties.
///
/// Mixin defines enabled and enabled state probing properties for interactive widgets.
#[widget_mixin]
pub struct InteractivityMix<P>(P);
context_var! {
static IS_ENABLED_VAR: bool = true;
}
/// If default interaction is allowed in the widget and its descendants.
///
/// This property sets the interactivity of the widget to [`ENABLED`] or [`DISABLED`], to probe the enabled state in `when` clauses
/// use [`is_enabled`] or [`is_disabled`]. To probe the a widget's state use [`interactivity`] value.
///
/// # Interactivity
///
/// Every widget has an [`interactivity`] value, it defines two *tiers* of disabled, the normal disabled blocks the default actions
/// of the widget, but still allows some interactions, such as a different cursor on hover or event an error tool-tip on click, the
/// second tier blocks all interaction with the widget. This property controls the *normal* disabled, to fully block interaction use
/// the [`interactive`] property.
///
/// # Disabled Visual
///
/// Widgets that are interactive should visually indicate when the normal interactions are disabled, you can use the [`is_disabled`]
/// state property in a when block to implement the *visually disabled* appearance of a widget.
///
/// The visual cue for the disabled state is usually a reduced contrast from content and background by *graying-out* the text and applying a
/// grayscale filter for image content. You should also consider adding *disabled interactions* that inform the user when the widget will be
/// enabled.
///
/// # Implicit
///
/// This property is included in all widgets by default, you don't need to import it to use it.
///
/// [`ENABLED`]: zero_ui_app::widget::info::Interactivity::ENABLED
/// [`DISABLED`]: zero_ui_app::widget::info::Interactivity::DISABLED
/// [`interactivity`]: zero_ui_app::widget::info::WidgetInfo::interactivity
/// [`interactive`]: fn@interactive
/// [`is_enabled`]: fn@is_enabled
/// [`is_disabled`]: fn@is_disabled
#[property(CONTEXT, default(true), widget_impl(InteractivityMix<P>))]
pub fn enabled(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
let child = match_node(
child,
clmv!(enabled, |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&enabled);
}
UiNodeOp::Info { info } => {
if !enabled.get() {
info.push_interactivity(Interactivity::DISABLED);
}
}
_ => {}
}),
);
nodes::with_context_var(child, IS_ENABLED_VAR, merge_var!(IS_ENABLED_VAR, enabled, |&a, &b| a && b))
}
/// Defines if any interaction is allowed in the widget and its descendants.
///
/// This property sets the interactivity of the widget to [`BLOCKED`] when `false`, widgets with blocked interactivity do not
/// receive any interaction event and behave like a background visual. To probe the widget state use [`interactivity`] value.
///
/// This property *enables* and *disables* interaction with the widget and its descendants without causing
/// a visual change like [`enabled`], it also blocks "disabled" interactions such as a different cursor or tool-tip for disabled buttons,
/// its use cases are more advanced then [`enabled`], it is mostly used when large parts of the screen are "not ready".
///
/// Note that this affects the widget where it is set and descendants, to disable interaction only in the widgets
/// inside `child` use the [`base::nodes::interactive_node`].
///
/// [`enabled`]: fn@enabled
/// [`BLOCKED`]: Interactivity::BLOCKED
/// [`interactivity`]: zero_ui_app::widget::info::WidgetInfo::interactivity
#[property(CONTEXT, default(true), widget_impl(InteractivityMix<P>))]
pub fn interactive(child: impl UiNode, interactive: impl IntoVar<bool>) -> impl UiNode {
let interactive = interactive.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_info(&interactive);
}
UiNodeOp::Info { info } => {
if !interactive.get() {
info.push_interactivity(Interactivity::BLOCKED);
}
}
_ => {}
})
}
/// If the widget is enabled for interaction.
///
/// This property is used only for probing the state. You can set the state using
/// the [`enabled`] property.
///
/// [`enabled`]: fn@enabled
/// [`WidgetInfo::allow_interaction`]: crate::widget_info::WidgetInfo::allow_interaction
#[property(EVENT, widget_impl(InteractivityMix<P>))]
pub fn is_enabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
vis_enabled_eq_state(child, state, true)
}
/// If the widget is disabled for interaction.
///
/// This property is used only for probing the state. You can set the state using
/// the [`enabled`] property.
///
/// This is the same as `!self.is_enabled`.
///
/// [`enabled`]: fn@enabled
#[property(EVENT, widget_impl(InteractivityMix<P>))]
pub fn is_disabled(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
vis_enabled_eq_state(child, state, false)
}
/// Visibility properties.
///
/// Mixin defines visibility and visibility state probing properties for all widgets.
#[widget_mixin]
pub struct VisibilityMix<P>(P);
fn vis_enabled_eq_state(child: impl UiNode, state: impl IntoVar<bool>, expected: bool) -> impl UiNode {
nodes::event_is_state(child, state, true, info::INTERACTIVITY_CHANGED_EVENT, move |args| {
if let Some((_, new)) = args.vis_enabled_change(WIDGET.id()) {
Some(new.is_vis_enabled() == expected)
} else {
None
}
})
}
/// Sets the widget visibility.
///
/// This property causes the widget to have the `visibility`, the widget actual visibility is computed, for example,
/// widgets that don't render anything are considered `Hidden` even if the visibility property is not set, this property
/// only forces the widget to layout and render according to the specified visibility.
///
/// To probe the visibility state of a widget in `when` clauses use [`is_visible`], [`is_hidden`] or [`is_collapsed`] in `when` clauses,
/// to probe a widget state use [`UiNode::with_context`] or [`WidgetInfo::visibility`].
///
/// # Implicit
///
/// This property is included in all widgets by default, you don't need to import it to use it.
///
/// [`is_visible`]: fn@is_visible
/// [`is_hidden`]: fn@is_hidden
/// [`is_collapsed`]: fn@is_collapsed
/// [`WidgetInfo::visibility`]: zero_ui_app::widget::info::WidgetInfo::visibility
#[property(CONTEXT, default(true), widget_impl(VisibilityMix<P>))]
pub fn visibility(child: impl UiNode, visibility: impl IntoVar<Visibility>) -> impl UiNode {
let visibility = visibility.into_var();
let mut prev_vis = Visibility::Visible;
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&visibility);
prev_vis = visibility.get();
}
UiNodeOp::Update { .. } => {
if let Some(vis) = visibility.get_new() {
use Visibility::*;
match (prev_vis, vis) {
(Collapsed, Visible) | (Visible, Collapsed) => {
WIDGET.layout().render();
}
(Hidden, Visible) | (Visible, Hidden) => {
WIDGET.render();
}
(Collapsed, Hidden) | (Hidden, Collapsed) => {
WIDGET.layout();
}
_ => {}
}
prev_vis = vis;
}
}
UiNodeOp::Measure { wm, desired_size } => {
if Visibility::Collapsed != visibility.get() {
*desired_size = child.measure(wm);
} else {
child.delegated();
}
}
UiNodeOp::Layout { wl, final_size } => {
if Visibility::Collapsed != visibility.get() {
*final_size = child.layout(wl);
} else {
wl.collapse();
child.delegated();
}
}
UiNodeOp::Render { frame } => match visibility.get() {
Visibility::Visible => child.render(frame),
Visibility::Hidden => frame.hide(|frame| child.render(frame)),
Visibility::Collapsed => {
child.delegated();
#[cfg(debug_assertions)]
{
tracing::error!(
"collapsed {} rendered, to fix, layout the widget, or `WidgetLayout::collapse_child` the widget",
WIDGET.trace_id()
)
}
}
},
UiNodeOp::RenderUpdate { update } => match visibility.get() {
Visibility::Visible => child.render_update(update),
Visibility::Hidden => update.hidden(|update| child.render_update(update)),
Visibility::Collapsed => {
child.delegated();
#[cfg(debug_assertions)]
{
tracing::error!(
"collapsed {} render-updated, to fix, layout the widget, or `WidgetLayout::collapse_child` the widget",
WIDGET.trace_id()
)
}
}
},
_ => {}
})
}
fn visibility_eq_state(child: impl UiNode, state: impl IntoVar<bool>, expected: Visibility) -> impl UiNode {
nodes::event_is_state(
child,
state,
expected == Visibility::Visible,
info::VISIBILITY_CHANGED_EVENT,
move |_| {
let tree = WINDOW.info();
let vis = tree.get(WIDGET.id()).map(|w| w.visibility()).unwrap_or(Visibility::Visible);
Some(vis == expected)
},
)
}
/// If the widget is [`Visible`](Visibility::Visible).
#[property(CONTEXT, widget_impl(VisibilityMix<P>))]
pub fn is_visible(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
visibility_eq_state(child, state, Visibility::Visible)
}
/// If the widget is [`Hidden`](Visibility::Hidden).
#[property(CONTEXT, widget_impl(VisibilityMix<P>))]
pub fn is_hidden(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
visibility_eq_state(child, state, Visibility::Hidden)
}
/// If the widget is [`Collapsed`](Visibility::Collapsed).
#[property(CONTEXT, widget_impl(VisibilityMix<P>))]
pub fn is_collapsed(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
visibility_eq_state(child, state, Visibility::Collapsed)
}
/// Defines if the widget only renders if it-s bounds intersects with the viewport auto-hide rectangle.
///
/// The auto-hide rect is usually *one viewport* of extra space around the viewport, so only widgets that transform
/// themselves very far need to set this, disabling auto-hide for an widget does not disable it for descendants.
///
/// # Examples
///
/// The example demonstrates a `container` that is *fixed* in the scroll viewport, it sets the `x` and `y` properties
/// to always stay in frame, but transforms set by a widget on itself always affects the [`inner_bounds`], the
/// [`outer_bounds`] will still be the transform set by the parent so the container may end-up auto-hidden.
///
/// Note that auto-hide is not disabled for the `content` widget, but it's [`outer_bounds`] is affected by the `container`
/// so it is auto-hidden correctly.
///
/// ```
/// # macro_rules! Container { ($($tt:tt)*) => { NilUiNode }}
/// # use zero_ui_app::widget::instance::*;
/// fn center_viewport(content: impl UiNode) -> impl UiNode {
/// Container! {
/// zero_ui::core::widget_base::can_auto_hide = false;
///
/// x = zero_ui::widgets::scroll::SCROLL_HORIZONTAL_OFFSET_VAR.map(|&fct| Length::Relative(fct) - 1.vw() * fct);
/// y = zero_ui::widgets::scroll::SCROLL_VERTICAL_OFFSET_VAR.map(|&fct| Length::Relative(fct) - 1.vh() * fct);
/// max_size = (1.vw(), 1.vh());
/// content_align = Align::CENTER;
///
/// content;
/// }
/// }
/// ```
///
/// [`outer_bounds`]: info::WidgetBoundsInfo::outer_bounds
/// [`inner_bounds`]: info::WidgetBoundsInfo::inner_bounds
#[property(CONTEXT, default(true), widget_impl(VisibilityMix<P>))]
pub fn can_auto_hide(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
let enabled = enabled.into_var();
match_node(child, move |_, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var(&enabled);
}
UiNodeOp::Update { .. } => {
if let Some(new) = enabled.get_new() {
if WIDGET.bounds().can_auto_hide() != new {
WIDGET.layout().render();
}
}
}
UiNodeOp::Layout { wl, .. } => {
wl.allow_auto_hide(enabled.get());
}
_ => {}
})
}
/// Hit-test visibility properties.
///
/// Mixin defines hit-test control state probing properties for all widgets.
#[widget_mixin]
pub struct HitTestMix<P>(P);
/// Defines if and how a widget is hit-tested.
///
/// See [`hit_test_mode`](fn@hit_test_mode) for more details.
#[derive(Copy, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum HitTestMode {
/// Widget is never hit.
///
/// This mode affects the entire UI branch, if set it disables hit-testing for the widget and all its descendants.
Disabled,
/// Widget is hit by any point that intersects the transformed inner bounds rectangle. If the widget is inlined
/// excludes the first row advance and the last row trailing space.
Bounds,
/// Default mode.
///
/// Same as `Bounds`, but also excludes the outside of rounded corners.
#[default]
RoundedBounds,
/// Every render primitive used for rendering the widget is hit-testable, the widget is hit only by
/// points that intersect visible parts of the render primitives.
///
/// Note that not all primitives implement pixel accurate hit-testing.
Visual,
}
impl HitTestMode {
/// Returns `true` if is any mode other then [`Disabled`].
///
/// [`Disabled`]: Self::Disabled
pub fn is_hit_testable(&self) -> bool {
!matches!(self, Self::Disabled)
}
/// Read-only context var with the contextual mode.
pub fn var() -> impl Var<HitTestMode> {
HIT_TEST_MODE_VAR.read_only()
}
}
impl fmt::Debug for HitTestMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "HitTestMode::")?;
}
match self {
Self::Disabled => write!(f, "Disabled"),
Self::Bounds => write!(f, "Bounds"),
Self::RoundedBounds => write!(f, "RoundedBounds"),
Self::Visual => write!(f, "Visual"),
}
}
}
impl_from_and_into_var! {
fn from(default_or_disabled: bool) -> HitTestMode {
if default_or_disabled {
HitTestMode::default()
} else {
HitTestMode::Disabled
}
}
}
context_var! {
static HIT_TEST_MODE_VAR: HitTestMode = HitTestMode::default();
}
/// Defines how the widget is hit-tested.
///
/// Hit-testing determines if a point intersects with the widget, the most common hit-test point is the mouse pointer.
/// By default widgets are hit by any point inside the widget area, excluding the outer corners if [`corner_radius`] is set,
/// this is very efficient, but assumes that the widget is *filled*, if the widget has visual *holes* the user may be able
/// to see another widget underneath but be unable to click on it.
///
/// If you have a widget with a complex shape or with *holes*, set this property to [`HitTestMode::Visual`] to enable the full
/// hit-testing power where all render primitives and clips used to render the widget are considered during hit-testing.
///
/// [`hit_testable`]: fn@hit_testable
/// [`corner_radius`]: fn@corner_radius
#[property(CONTEXT, default(HIT_TEST_MODE_VAR), widget_impl(HitTestMix<P>))]
pub fn hit_test_mode(child: impl UiNode, mode: impl IntoVar<HitTestMode>) -> impl UiNode {
let child = match_node(child, |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render(&HitTestMode::var());
}
UiNodeOp::Render { frame } => match HitTestMode::var().get() {
HitTestMode::Disabled => {
frame.with_hit_tests_disabled(|frame| child.render(frame));
}
HitTestMode::Visual => frame.with_auto_hit_test(true, |frame| child.render(frame)),
_ => frame.with_auto_hit_test(false, |frame| child.render(frame)),
},
UiNodeOp::RenderUpdate { update } => {
update.with_auto_hit_test(matches!(HitTestMode::var().get(), HitTestMode::Visual), |update| {
child.render_update(update)
});
}
_ => {}
});
nodes::with_context_var(
child,
HIT_TEST_MODE_VAR,
merge_var!(HIT_TEST_MODE_VAR, mode.into_var(), |&a, &b| match (a, b) {
(HitTestMode::Disabled, _) | (_, HitTestMode::Disabled) => HitTestMode::Disabled,
(_, b) => b,
}),
)
}
/// If the widget is visible for hit-tests.
///
/// This property is used only for probing the state. You can set the state using
/// the [`hit_test_mode`] property.
///
/// [`hit_testable`]: fn@hit_testable
/// [`hit_test_mode`]: fn@hit_test_mode
#[property(EVENT, widget_impl(HitTestMix<P>))]
pub fn is_hit_testable(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
nodes::bind_is_state(child, HIT_TEST_MODE_VAR.map(|m| m.is_hit_testable()), state)
}
/// Defines what node list methods can run in parallel in the widget and descendants.
///
/// This property sets the [`PARALLEL_VAR`] that is used by [`UiNodeList`] implementers to toggle parallel processing.
///
/// See also `WINDOWS.parallel` to define parallelization in multi-window apps.
///
/// [`UiNode`]: zero_ui_app::widget::instance::UiNodeList
#[property(CONTEXT, default(PARALLEL_VAR))]
pub fn parallel(child: impl UiNode, enabled: impl IntoVar<Parallel>) -> impl UiNode {
nodes::with_context_var(child, PARALLEL_VAR, enabled)
}
/// Border control properties.
#[widget_mixin]
pub struct BorderMix<P>(P);
/// Corner radius of widget and inner widgets.
///
/// The [`Default`] value is calculated to fit inside the parent widget corner curve, see [`corner_radius_fit`].
///
/// [`Default`]: zero_ui_layout::units::Length::Default
/// [`corner_radius_fit`]: fn@corner_radius_fit
#[property(CONTEXT, default(CORNER_RADIUS_VAR), widget_impl(BorderMix<P>))]
pub fn corner_radius(child: impl UiNode, radius: impl IntoVar<border::CornerRadius>) -> impl UiNode {
let child = match_node(child, move |child, op| {
if let UiNodeOp::Layout { wl, final_size } = op {
*final_size = border::BORDER.with_corner_radius(|| child.layout(wl));
}
});
nodes::with_context_var(child, CORNER_RADIUS_VAR, radius)
}
/// Defines how the [`corner_radius`] is computed for each usage.
///
/// Nesting borders with round corners need slightly different radius values to perfectly fit, the [`BORDER`]
/// coordinator can adjusts the radius inside each border to match the inside curve of the border.
///
/// Sets the [`CORNER_RADIUS_FIT_VAR`].
///
/// [`corner_radius`]: fn@corner_radius
/// [`BORDER`]: zero_ui_app::widget::border::BORDER
#[property(CONTEXT, default(CORNER_RADIUS_FIT_VAR), widget_impl(BorderMix<P>))]
pub fn corner_radius_fit(child: impl UiNode, fit: impl IntoVar<border::CornerRadiusFit>) -> impl UiNode {
nodes::with_context_var(child, CORNER_RADIUS_FIT_VAR, fit)
}
/// Position of a widget borders in relation to the widget fill.
///
/// This property defines how much the widget's border offsets affect the layout of the fill content, by default
/// (0%) the fill content stretchers *under* the borders and is clipped by the [`corner_radius`], in the other end
/// of the scale (100%), the fill content is positioned *inside* the borders and clipped by the adjusted [`corner_radius`]
/// that fits the insider of the inner most border.
///
/// Note that widget's content is always *inside* the borders, this property only affects the *fill* properties content, such as a
/// the image in a background image.
///
/// Fill property implementers, see [`nodes::fill_node`], a helper function for quickly implementing support for `border_align`.
///
/// Sets the [`BORDER_ALIGN_VAR`].
///
/// [`corner_radius`]: fn@corner_radius
#[property(CONTEXT, default(BORDER_ALIGN_VAR), widget_impl(BorderMix<P>))]
pub fn border_align(child: impl UiNode, align: impl IntoVar<zero_ui_layout::units::FactorSideOffsets>) -> impl UiNode {
nodes::with_context_var(child, BORDER_ALIGN_VAR, align)
}
/// If the border is rendered over the fill and child visuals.
///
/// Is `true` by default, if set to `false` the borders will render under the fill. Note that
/// this means the border will be occluded by the *background* if [`border_align`] is not set to `1.fct()`.
///
/// Sets the [`BORDER_OVER_VAR`].
///
/// [`border_align`]: fn@border_align
#[property(CONTEXT, default(BORDER_OVER_VAR), widget_impl(BorderMix<P>))]
pub fn border_over(child: impl UiNode, over: impl IntoVar<bool>) -> impl UiNode {
nodes::with_context_var(child, BORDER_OVER_VAR, over)
}