Started refactoring main crate.
This commit is contained in:
parent
a61b8d7959
commit
db7042f0c5
31
Cargo.toml
31
Cargo.toml
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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" }
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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::*;
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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" }
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"] }
|
|
@ -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)
|
||||
}
|
|
@ -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"] }
|
|
@ -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(¬e);
|
||||
|
||||
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 = ¬es.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);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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" }
|
|
@ -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))
|
||||
}
|
|
@ -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) },
|
||||
)
|
||||
}
|
|
@ -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" }
|
|
@ -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));
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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"
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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());
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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"
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
//! Debug properties and inspector implementation.
|
||||
|
||||
pub mod debug;
|
|
@ -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
|
@ -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" }
|
|
@ -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)+
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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"
|
|
@ -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();
|
||||
}
|
|
@ -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" }
|
|
@ -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)
|
||||
}
|
|
@ -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" }
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"]
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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::*;
|
|
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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)]
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
|
@ -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>>>);
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue