#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]
zng_wgt::enable_widget_macros!();
use std::ops;
use std::{error::Error, fmt, marker::PhantomData, sync::Arc};
use colors::BASE_COLOR_VAR;
use task::parking_lot::Mutex;
use zng_ext_font::FontNames;
use zng_ext_input::{
gesture::CLICK_EVENT,
mouse::{ClickMode, MOUSE_INPUT_EVENT},
pointer_capture::CaptureMode,
};
use zng_ext_l10n::lang;
use zng_var::{AnyVar, AnyVarValue, BoxedAnyVar, Var, VarIsReadOnlyError};
use zng_wgt::{align, border, border_align, border_over, corner_radius, hit_test_mode, is_inited, prelude::*, Wgt, ICONS};
use zng_wgt_access::{access_role, accessible, AccessRole};
use zng_wgt_container::{child_align, child_end, child_start, padding};
use zng_wgt_fill::background_color;
use zng_wgt_filter::opacity;
use zng_wgt_input::{click_mode, is_hovered, pointer_capture::capture_pointer_on_init};
use zng_wgt_layer::popup::{PopupState, POPUP};
use zng_wgt_size_offset::{size, x, y};
use zng_wgt_style::{impl_style_fn, style_fn, Style};
pub mod cmd;
#[widget($crate::Toggle)]
pub struct Toggle(zng_wgt_button::Button);
impl Toggle {
fn widget_intrinsic(&mut self) {
self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
widget_set! {
self;
style_base_fn = style_fn!(|_| DefaultStyle!());
}
}
}
impl_style_fn!(Toggle);
context_var! {
pub static IS_CHECKED_VAR: Option<bool> = false;
pub static IS_TRISTATE_VAR: bool = false;
}
#[property(CONTEXT, default(false), widget_impl(Toggle))]
pub fn checked(child: impl UiNode, checked: impl IntoVar<bool>) -> impl UiNode {
let checked = checked.into_var();
let mut _toggle_handle = CommandHandle::dummy();
let mut access_handle = VarHandle::dummy();
let node = match_node(
child,
clmv!(checked, |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_event(&CLICK_EVENT);
_toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
}
UiNodeOp::Deinit => {
_toggle_handle = CommandHandle::dummy();
access_handle = VarHandle::dummy();
}
UiNodeOp::Info { info } => {
if let Some(mut a) = info.access() {
if access_handle.is_dummy() {
access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
}
a.set_checked(Some(checked.get()));
}
}
UiNodeOp::Event { update } => {
child.event(update);
if let Some(args) = CLICK_EVENT.on(update) {
if args.is_primary()
&& checked.capabilities().contains(VarCapability::MODIFY)
&& !args.propagation().is_stopped()
&& args.is_enabled(WIDGET.id())
{
args.propagation().stop();
let _ = checked.set(!checked.get());
}
} else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
if let Some(b) = args.param::<bool>() {
args.propagation().stop();
let _ = checked.set(*b);
} else if let Some(b) = args.param::<Option<bool>>() {
if let Some(b) = b {
args.propagation().stop();
let _ = checked.set(*b);
}
} else if args.param.is_none() {
args.propagation().stop();
let _ = checked.set(!checked.get());
}
}
}
_ => {}
}),
);
with_context_var(node, IS_CHECKED_VAR, checked.map_into())
}
#[property(CONTEXT + 1, default(None), widget_impl(Toggle))]
pub fn checked_opt(child: impl UiNode, checked: impl IntoVar<Option<bool>>) -> impl UiNode {
let checked = checked.into_var();
let mut _toggle_handle = CommandHandle::dummy();
let mut access_handle = VarHandle::dummy();
let node = match_node(
child,
clmv!(checked, |child, op| match op {
UiNodeOp::Init => {
WIDGET.sub_event(&CLICK_EVENT);
_toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
}
UiNodeOp::Deinit => {
_toggle_handle = CommandHandle::dummy();
access_handle = VarHandle::dummy();
}
UiNodeOp::Info { info } => {
if let Some(mut a) = info.access() {
if access_handle.is_dummy() {
access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
}
a.set_checked(checked.get());
}
}
UiNodeOp::Event { update } => {
child.event(update);
let mut cycle = false;
if let Some(args) = CLICK_EVENT.on(update) {
if args.is_primary()
&& checked.capabilities().contains(VarCapability::MODIFY)
&& !args.propagation().is_stopped()
&& args.is_enabled(WIDGET.id())
{
args.propagation().stop();
cycle = true;
}
} else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
if let Some(b) = args.param::<bool>() {
args.propagation().stop();
let _ = checked.set(Some(*b));
} else if let Some(b) = args.param::<Option<bool>>() {
if IS_TRISTATE_VAR.get() {
args.propagation().stop();
let _ = checked.set(*b);
} else if let Some(b) = b {
args.propagation().stop();
let _ = checked.set(Some(*b));
}
} else if args.param.is_none() {
args.propagation().stop();
cycle = true;
}
}
if cycle {
if IS_TRISTATE_VAR.get() {
let _ = checked.set(match checked.get() {
Some(true) => None,
Some(false) => Some(true),
None => Some(false),
});
} else {
let _ = checked.set(match checked.get() {
Some(true) | None => Some(false),
Some(false) => Some(true),
});
}
}
}
_ => {}
}),
);
with_context_var(node, IS_CHECKED_VAR, checked)
}
#[property(CONTEXT, default(IS_TRISTATE_VAR), widget_impl(Toggle))]
pub fn tristate(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, IS_TRISTATE_VAR, enabled)
}
#[property(EVENT, widget_impl(Toggle))]
pub fn is_checked(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
bind_state(child, IS_CHECKED_VAR.map(|s| *s == Some(true)), state)
}
#[property(CONTEXT+2, widget_impl(Toggle))]
pub fn value<T: VarValue>(child: impl UiNode, value: impl IntoVar<T>) -> impl UiNode {
value_impl(child, value.into_var().boxed_any())
}
fn value_impl(child: impl UiNode, value: BoxedAnyVar) -> impl UiNode {
fn select(value: &dyn AnyVarValue) -> bool {
let selector = SELECTOR.get();
match selector.select(value.clone_boxed()) {
Ok(()) => true,
Err(e) => {
let selected = selector.is_selected(value);
if selected {
tracing::error!("selected `{value:?}` with error, {e}");
} else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
} else {
tracing::error!("failed to select `{value:?}`, {e}");
}
selected
}
}
}
fn deselect(value: &dyn AnyVarValue) -> bool {
let selector = SELECTOR.get();
match selector.deselect(value) {
Ok(()) => true,
Err(e) => {
let deselected = !selector.is_selected(value);
if deselected {
tracing::error!("deselected `{value:?}` with error, {e}");
} else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
} else {
tracing::error!("failed to deselect `{value:?}`, {e}");
}
deselected
}
}
}
fn is_selected(value: &dyn AnyVarValue) -> bool {
SELECTOR.get().is_selected(value)
}
let checked = var(Some(false));
let child = with_context_var(child, IS_CHECKED_VAR, checked.clone());
let mut prev_value = None::<Box<dyn AnyVarValue>>;
let mut _click_handle = None;
let mut _toggle_handle = CommandHandle::dummy();
let mut _select_handle = CommandHandle::dummy();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
let id = WIDGET.id();
WIDGET.sub_var(&value).sub_var(&DESELECT_ON_NEW_VAR).sub_var(&checked);
SELECTOR.get().subscribe();
value.with_any(&mut |value| {
let selected = if SELECT_ON_INIT_VAR.get() {
select(value)
} else {
is_selected(value)
};
checked.set(Some(selected));
if DESELECT_ON_DEINIT_VAR.get() {
prev_value = Some(value.clone_boxed());
}
});
_click_handle = Some(CLICK_EVENT.subscribe(id));
_toggle_handle = cmd::TOGGLE_CMD.scoped(id).subscribe(true);
_select_handle = cmd::SELECT_CMD.scoped(id).subscribe(true);
}
UiNodeOp::Deinit => {
if checked.get() == Some(true) && DESELECT_ON_DEINIT_VAR.get() {
value.with_any(&mut |value| {
if deselect(value) {
checked.set(Some(false));
}
});
}
prev_value = None;
_click_handle = None;
_toggle_handle = CommandHandle::dummy();
_select_handle = CommandHandle::dummy();
}
UiNodeOp::Event { update } => {
child.event(update);
if let Some(args) = CLICK_EVENT.on(update) {
if args.is_primary() && !args.propagation().is_stopped() && args.is_enabled(WIDGET.id()) {
args.propagation().stop();
value.with_any(&mut |value| {
let selected = if checked.get() == Some(true) {
!deselect(value)
} else {
select(value)
};
checked.set(Some(selected))
});
}
} else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
if args.param.is_none() {
args.propagation().stop();
value.with_any(&mut |value| {
let selected = if checked.get() == Some(true) {
!deselect(value)
} else {
select(value)
};
checked.set(Some(selected))
});
} else {
let s = if let Some(s) = args.param::<Option<bool>>() {
Some(s.unwrap_or(false))
} else {
args.param::<bool>().copied()
};
if let Some(s) = s {
args.propagation().stop();
value.with_any(&mut |value| {
let selected = if s { select(value) } else { !deselect(value) };
checked.set(Some(selected))
});
}
}
} else if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update) {
if args.param.is_none() {
args.propagation().stop();
value.with_any(&mut |value| {
let selected = checked.get() == Some(true);
if !selected && select(value) {
checked.set(Some(true));
}
});
}
}
}
UiNodeOp::Update { .. } => {
let mut selected = None;
value.with_new_any(&mut |new| {
selected = Some(if checked.get() == Some(true) && SELECT_ON_NEW_VAR.get() {
select(new)
} else {
is_selected(new)
});
if let Some(prev) = prev_value.take() {
if DESELECT_ON_NEW_VAR.get() {
deselect(&*prev);
prev_value = Some(new.clone_boxed());
}
}
});
let selected = selected.unwrap_or_else(|| {
let mut s = false;
value.with_any(&mut |v| {
s = is_selected(v);
});
s
});
checked.set(selected);
if DESELECT_ON_NEW_VAR.get() && selected {
if prev_value.is_none() {
prev_value = Some(value.get_any());
}
} else {
prev_value = None;
}
if let Some(Some(true)) = checked.get_new() {
if SCROLL_ON_SELECT_VAR.get() {
use zng_wgt_scroll::cmd::*;
scroll_to(WIDGET.id(), ScrollToMode::minimal(10));
}
}
}
_ => {}
})
}
#[property(CONTEXT, default(SCROLL_ON_SELECT_VAR), widget_impl(Toggle))]
pub fn scroll_on_select(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, SCROLL_ON_SELECT_VAR, enabled)
}
#[property(CONTEXT, default(Selector::nil()), widget_impl(Toggle))]
pub fn selector(child: impl UiNode, selector: impl IntoValue<Selector>) -> impl UiNode {
let mut _select_handle = CommandHandle::dummy();
let child = match_node(child, move |c, op| match op {
UiNodeOp::Init => {
_select_handle = cmd::SELECT_CMD.scoped(WIDGET.id()).subscribe(true);
}
UiNodeOp::Info { info } => {
if let Some(mut info) = info.access() {
info.set_role(AccessRole::RadioGroup);
}
}
UiNodeOp::Deinit => {
_select_handle = CommandHandle::dummy();
}
UiNodeOp::Event { update } => {
c.event(update);
if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update) {
if let Some(p) = args.param::<cmd::SelectOp>() {
args.propagation().stop();
p.call();
}
}
}
_ => {}
});
with_context_local(child, &SELECTOR, selector)
}
#[property(CONTEXT, default(SELECT_ON_INIT_VAR), widget_impl(Toggle))]
pub fn select_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, SELECT_ON_INIT_VAR, enabled)
}
#[property(CONTEXT, default(DESELECT_ON_DEINIT_VAR), widget_impl(Toggle))]
pub fn deselect_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, DESELECT_ON_DEINIT_VAR, enabled)
}
#[property(CONTEXT, default(SELECT_ON_NEW_VAR), widget_impl(Toggle))]
pub fn select_on_new(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, SELECT_ON_NEW_VAR, enabled)
}
#[property(CONTEXT, default(DESELECT_ON_NEW_VAR), widget_impl(Toggle))]
pub fn deselect_on_new(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
with_context_var(child, DESELECT_ON_NEW_VAR, enabled)
}
context_local! {
pub static SELECTOR: Selector = Selector::nil();
}
context_var! {
pub static SELECT_ON_INIT_VAR: bool = false;
pub static DESELECT_ON_DEINIT_VAR: bool = false;
pub static SELECT_ON_NEW_VAR: bool = true;
pub static DESELECT_ON_NEW_VAR: bool = false;
pub static SCROLL_ON_SELECT_VAR: bool = true;
}
pub trait SelectorImpl: Send + 'static {
fn subscribe(&self);
fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError>;
fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError>;
fn is_selected(&self, value: &dyn AnyVarValue) -> bool;
}
#[derive(Clone)]
pub struct Selector(Arc<Mutex<dyn SelectorImpl>>);
impl Selector {
pub fn new(selector: impl SelectorImpl) -> Self {
Self(Arc::new(Mutex::new(selector)))
}
pub fn nil() -> Self {
struct NilSel;
impl SelectorImpl for NilSel {
fn subscribe(&self) {}
fn select(&mut self, _: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
Err(SelectorError::custom_str("no contextual `selector`"))
}
fn deselect(&mut self, _: &dyn AnyVarValue) -> Result<(), SelectorError> {
Ok(())
}
fn is_selected(&self, __r: &dyn AnyVarValue) -> bool {
false
}
}
Self::new(NilSel)
}
pub fn single<T>(selection: impl IntoVar<T>) -> Self
where
T: VarValue,
{
struct SingleSel<T, S> {
selection: S,
_type: PhantomData<T>,
}
impl<T, S> SelectorImpl for SingleSel<T, S>
where
T: VarValue,
S: Var<T>,
{
fn subscribe(&self) {
WIDGET.sub_var(&self.selection);
}
fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
match value.into_any().downcast::<T>() {
Ok(value) => match self.selection.set(*value) {
Ok(_) => Ok(()),
Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
},
Err(_) => Err(SelectorError::WrongType),
}
}
fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
if self.is_selected(value) {
Err(SelectorError::CannotClear)
} else {
Ok(())
}
}
fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
match value.as_any().downcast_ref::<T>() {
Some(value) => self.selection.with(|t| t == value),
None => false,
}
}
}
Self::new(SingleSel {
selection: selection.into_var(),
_type: PhantomData,
})
}
pub fn single_opt<T>(selection: impl IntoVar<Option<T>>) -> Self
where
T: VarValue,
{
struct SingleOptSel<T, S> {
selection: S,
_type: PhantomData<T>,
}
impl<T, S> SelectorImpl for SingleOptSel<T, S>
where
T: VarValue,
S: Var<Option<T>>,
{
fn subscribe(&self) {
WIDGET.sub_var(&self.selection);
}
fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
match value.into_any().downcast::<T>() {
Ok(value) => match self.selection.set(Some(*value)) {
Ok(_) => Ok(()),
Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
},
Err(value) => match value.downcast::<Option<T>>() {
Ok(value) => match self.selection.set(*value) {
Ok(_) => Ok(()),
Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
},
Err(_) => Err(SelectorError::WrongType),
},
}
}
fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
match value.as_any().downcast_ref::<T>() {
Some(value) => {
if self.selection.with(|t| t.as_ref() == Some(value)) {
match self.selection.set(None) {
Ok(_) => Ok(()),
Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
}
} else {
Ok(())
}
}
None => match value.as_any().downcast_ref::<Option<T>>() {
Some(value) => {
if self.selection.with(|t| t == value) {
if value.is_none() {
Ok(())
} else {
match self.selection.set(None) {
Ok(_) => Ok(()),
Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
}
}
} else {
Ok(())
}
}
None => Ok(()),
},
}
}
fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
match value.as_any().downcast_ref::<T>() {
Some(value) => self.selection.with(|t| t.as_ref() == Some(value)),
None => match value.as_any().downcast_ref::<Option<T>>() {
Some(value) => self.selection.with(|t| t == value),
None => false,
},
}
}
}
Self::new(SingleOptSel {
selection: selection.into_var(),
_type: PhantomData,
})
}
pub fn bitflags<T>(selection: impl IntoVar<T>) -> Self
where
T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
{
struct BitflagsSel<T, S> {
selection: S,
_type: PhantomData<T>,
}
impl<T, S> SelectorImpl for BitflagsSel<T, S>
where
T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
S: Var<T>,
{
fn subscribe(&self) {
WIDGET.sub_var(&self.selection);
}
fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
match value.into_any().downcast::<T>() {
Ok(value) => self
.selection
.modify(move |m| {
let value = *value;
let new = m.as_ref().clone() | value;
if m.as_ref() != &new {
m.set(new);
}
})
.map_err(|_| SelectorError::ReadOnly),
Err(_) => Err(SelectorError::WrongType),
}
}
fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
match value.as_any().downcast_ref::<T>() {
Some(value) => self
.selection
.modify(clmv!(value, |m| {
let new = m.as_ref().clone() & !value;
if m.as_ref() != &new {
m.set(new);
}
}))
.map_err(|_| SelectorError::ReadOnly),
None => Err(SelectorError::WrongType),
}
}
fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
match value.as_any().downcast_ref::<T>() {
Some(value) => &(self.selection.get() & value.clone()) == value,
None => false,
}
}
}
Self::new(BitflagsSel {
selection: selection.into_var(),
_type: PhantomData,
})
}
pub fn subscribe(&self) {
self.0.lock().subscribe();
}
pub fn select(&self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
self.0.lock().select(value)
}
pub fn deselect(&self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
self.0.lock().deselect(value)
}
pub fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
self.0.lock().is_selected(value)
}
}
impl<S: SelectorImpl> From<S> for Selector {
fn from(sel: S) -> Self {
Selector::new(sel)
}
}
impl fmt::Debug for Selector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Selector(_)")
}
}
impl PartialEq for Selector {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Debug, Clone)]
pub enum SelectorError {
WrongType,
ReadOnly,
CannotClear,
Custom(Arc<dyn Error + Send + Sync>),
}
impl SelectorError {
pub fn custom_str(str: impl Into<String>) -> SelectorError {
let str = str.into();
let e: Box<dyn Error + Send + Sync> = str.into();
let e: Arc<dyn Error + Send + Sync> = e.into();
SelectorError::Custom(e)
}
}
impl fmt::Display for SelectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SelectorError::WrongType => write!(f, "wrong value type for selector"),
SelectorError::ReadOnly => write!(f, "selection is read-only"),
SelectorError::CannotClear => write!(f, "selection cannot be empty"),
SelectorError::Custom(e) => fmt::Display::fmt(e, f),
}
}
}
impl Error for SelectorError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
SelectorError::WrongType => None,
SelectorError::ReadOnly => None,
SelectorError::CannotClear => None,
SelectorError::Custom(e) => Some(&**e),
}
}
}
impl From<VarIsReadOnlyError> for SelectorError {
fn from(_: VarIsReadOnlyError) -> Self {
SelectorError::ReadOnly
}
}
#[widget($crate::DefaultStyle)]
pub struct DefaultStyle(zng_wgt_button::DefaultStyle);
impl DefaultStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
replace = true;
when *#is_checked {
background_color = BASE_COLOR_VAR.shade(2);
border = {
widths: 1,
sides: BASE_COLOR_VAR.shade_into(2),
};
}
}
}
}
#[widget($crate::LightStyle)]
pub struct LightStyle(zng_wgt_button::LightStyle);
impl LightStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
when *#is_checked {
#[easing(0.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
}
}
}
}
#[widget($crate::CheckStyle)]
pub struct CheckStyle(Style);
impl CheckStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
replace = true;
child_start = {
node: {
let parent_hovered = var(false);
is_hovered(checkmark_visual(parent_hovered.clone()), parent_hovered)
},
spacing: CHECK_SPACING_VAR,
};
access_role = AccessRole::CheckBox;
}
}
}
context_var! {
pub static CHECK_SPACING_VAR: Length = 4;
}
#[property(CONTEXT, default(CHECK_SPACING_VAR), widget_impl(CheckStyle))]
pub fn check_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
with_context_var(child, CHECK_SPACING_VAR, spacing)
}
fn checkmark_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
let checked = ICONS.get_or(["toggle.checked", "check"], || {
zng_wgt_text::Text! {
txt = "✓";
font_family = FontNames::system_ui(&lang!(und));
txt_align = Align::CENTER;
}
});
let indeterminate = ICONS.get_or(["toggle.indeterminate"], || {
zng_wgt::Wgt! {
align = Align::CENTER;
background_color = zng_wgt_text::FONT_COLOR_VAR;
size = (6, 2);
corner_radius = 0;
}
});
zng_wgt_container::Container! {
hit_test_mode = false;
accessible = false;
size = 1.2.em();
corner_radius = 0.1.em();
align = Align::CENTER;
#[easing(150.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
when *#{parent_hovered} {
#[easing(0.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
}
when #{IS_CHECKED_VAR}.is_none() {
child = indeterminate;
}
when *#{IS_CHECKED_VAR} == Some(true) {
child = checked;
#[easing(0.ms())]
background_color = colors::ACCENT_COLOR_VAR.shade(-1);
}
}
}
#[widget($crate::ComboStyle)]
pub struct ComboStyle(DefaultStyle);
impl ComboStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
replace = true;
access_role = AccessRole::ComboBox;
child_align = Align::FILL;
border_over = false;
border_align = 1.fct();
padding = -1;
checked = var(false);
child_end = {
node: combomark_visual(),
spacing: COMBO_SPACING_VAR,
};
click_mode = ClickMode::press();
zng_wgt_button::style_fn = Style! {
click_mode = ClickMode::default();
corner_radius = (4, 0, 0, 4);
};
zng_wgt_layer::popup::style_fn = Style! {
zng_wgt_button::style_fn = Style! {
click_mode = ClickMode::release();
corner_radius = 0;
padding = 2;
border = unset!;
};
crate::style_fn = Style! {
click_mode = ClickMode::release();
corner_radius = 0;
padding = 2;
border = unset!;
};
capture_pointer_on_init = CaptureMode::Subtree;
#[easing(100.ms())]
opacity = 0.pct();
#[easing(100.ms())]
y = -10;
when *#is_inited {
opacity = 100.pct();
y = 0;
}
zng_wgt_layer::popup::close_delay = 100.ms();
when *#zng_wgt_layer::popup::is_close_delaying {
opacity = 0.pct();
y = -10;
}
};
}
}
}
context_var! {
pub static COMBO_SPACING_VAR: Length = 0;
}
#[property(CONTEXT, default(COMBO_SPACING_VAR), widget_impl(ComboStyle))]
pub fn combo_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
with_context_var(child, COMBO_SPACING_VAR, spacing)
}
#[property(CHILD, widget_impl(Toggle))]
pub fn checked_popup(child: impl UiNode, popup: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
let popup = popup.into_var();
let mut state = var(PopupState::Closed).read_only();
let mut _state_handle = VarHandle::dummy();
match_node(child, move |_, op| {
let new = match op {
UiNodeOp::Init => {
WIDGET.sub_var(&IS_CHECKED_VAR).sub_event(&MOUSE_INPUT_EVENT);
IS_CHECKED_VAR.get()
}
UiNodeOp::Deinit => {
_state_handle = VarHandle::dummy();
Some(false)
}
UiNodeOp::Event { update } => {
if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
if args.is_mouse_down() && args.is_primary() && IS_CHECKED_VAR.get() == Some(true) {
args.propagation().stop();
cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
}
}
None
}
UiNodeOp::Update { .. } => {
if let Some(s) = state.get_new() {
if matches!(s, PopupState::Closed) {
if IS_CHECKED_VAR.get() != Some(false) {
cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
}
_state_handle = VarHandle::dummy();
}
None
} else {
IS_CHECKED_VAR.get_new().map(|o| o.unwrap_or(false))
}
}
_ => None,
};
if let Some(open) = new {
if open {
if matches!(state.get(), PopupState::Closed) {
state = POPUP.open(popup.get()(()));
_state_handle = state.subscribe(UpdateOp::Update, WIDGET.id());
}
} else if let PopupState::Open(id) = state.get() {
POPUP.close_id(id);
}
}
})
}
fn combomark_visual() -> impl UiNode {
let dropdown = ICONS.get_or(
["toggle.dropdown", "material/rounded/keyboard-arrow-down", "keyboard-arrow-down"],
combomark_visual_fallback,
);
Wgt! {
size = 12;
zng_wgt_fill::background = dropdown;
align = Align::CENTER;
zng_wgt_transform::rotate_x = 0.deg();
when #is_checked {
zng_wgt_transform::rotate_x = 180.deg();
}
}
}
fn combomark_visual_fallback() -> impl UiNode {
let color_key = FrameValueKey::new_unique();
let mut size = PxSize::zero();
let mut bounds = PxBox::zero();
let mut transform = PxTransform::identity();
fn layout() -> (PxSize, PxTransform, PxBox) {
let size = Size::from(8).layout();
let center = size.to_vector() * 0.5.fct();
let transform = Transform::new_translate(-center.x, -center.y)
.rotate(45.deg())
.scale_x(0.7)
.translate(center.x, center.y)
.translate_x(Length::from(2).layout_x())
.layout();
let bounds = transform.outer_transformed(PxBox::from_size(size)).unwrap_or_default();
(size, transform, bounds)
}
match_node_leaf(move |op| match op {
UiNodeOp::Init => {
WIDGET.sub_var_render_update(&zng_wgt_text::FONT_COLOR_VAR);
}
UiNodeOp::Measure { desired_size, .. } => {
let (s, _, _) = layout();
*desired_size = s;
}
UiNodeOp::Layout { final_size, .. } => {
(size, transform, bounds) = layout();
*final_size = size;
}
UiNodeOp::Render { frame } => {
let mut clip = bounds.to_rect();
clip.size.height *= 0.5.fct();
clip.origin.y += clip.size.height;
frame.push_clip_rect(clip, false, false, |frame| {
frame.push_reference_frame((WIDGET.id(), 0).into(), transform.into(), false, false, |frame| {
frame.push_color(PxRect::from_size(size), color_key.bind_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
})
});
}
UiNodeOp::RenderUpdate { update } => {
update.update_color_opt(color_key.update_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
}
_ => {}
})
}
#[widget($crate::SwitchStyle)]
pub struct SwitchStyle(Style);
impl SwitchStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
replace = true;
child_start = {
node: {
let parent_hovered = var(false);
is_hovered(switch_visual(parent_hovered.clone()), parent_hovered)
},
spacing: SWITCH_SPACING_VAR,
};
}
}
}
context_var! {
pub static SWITCH_SPACING_VAR: Length = 2;
}
#[property(CONTEXT, default(SWITCH_SPACING_VAR), widget_impl(SwitchStyle))]
pub fn switch_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
with_context_var(child, SWITCH_SPACING_VAR, spacing)
}
fn switch_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
zng_wgt_container::Container! {
hit_test_mode = false;
size = (2.em(), 1.em());
align = Align::CENTER;
corner_radius = 1.em();
padding = 2;
child = Wgt! {
size = 1.em() - Length::from(4);
align = Align::LEFT;
background_color = zng_wgt_text::FONT_COLOR_VAR;
#[easing(150.ms())]
x = 0.em();
when *#is_checked {
x = 1.em();
}
};
#[easing(150.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
when *#{parent_hovered} {
#[easing(0.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
}
when #is_checked {
background_color = colors::ACCENT_COLOR_VAR.shade(-1);
}
}
}
#[widget($crate::RadioStyle)]
pub struct RadioStyle(Style);
impl RadioStyle {
fn widget_intrinsic(&mut self) {
widget_set! {
self;
replace = true;
access_role = AccessRole::Radio;
child_start = {
node: {
let parent_hovered = var(false);
is_hovered(radio_visual(parent_hovered.clone()), parent_hovered)
},
spacing: RADIO_SPACING_VAR,
};
}
}
}
context_var! {
pub static RADIO_SPACING_VAR: Length = 2;
}
#[property(CONTEXT, default(RADIO_SPACING_VAR), widget_impl(RadioStyle))]
pub fn radio_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
with_context_var(child, RADIO_SPACING_VAR, spacing)
}
fn radio_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
Wgt! {
hit_test_mode = false;
size = 0.9.em();
corner_radius = 0.9.em();
align = Align::CENTER;
border_align = 100.pct();
#[easing(150.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
when *#{parent_hovered} {
#[easing(0.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
}
when *#is_checked {
border = {
widths: 2,
sides: colors::ACCENT_COLOR_VAR.shade_into(-2),
};
#[easing(0.ms())]
background_color = zng_wgt_text::FONT_COLOR_VAR;
}
}
}