use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, Mutex, RwLock};
use crate::div::{Div, ElementBuilder, ElementRef, ElementTypeId};
use crate::element::RenderProps;
use crate::tree::{LayoutNodeId, LayoutTree};
use blinc_animation::{
AnimatedKeyframe, AnimatedTimeline, AnimatedValue, Easing, SchedulerHandle, SpringConfig,
};
use blinc_core::reactive::SignalId;
pub use crate::motion::SharedAnimatedValue;
pub type SharedAnimatedTimeline = Arc<Mutex<AnimatedTimeline>>;
pub type SharedKeyframeTrack = Arc<Mutex<AnimatedKeyframe>>;
#[derive(Clone)]
pub struct TimelineHandle {
inner: SharedAnimatedTimeline,
}
impl TimelineHandle {
pub fn get(&self, entry_id: blinc_animation::TimelineEntryId) -> Option<f32> {
self.inner.lock().unwrap().get(entry_id)
}
pub fn restart(&self) {
self.inner.lock().unwrap().restart();
}
pub fn start(&self) {
self.inner.lock().unwrap().start();
}
pub fn stop(&self) {
self.inner.lock().unwrap().stop();
}
pub fn is_playing(&self) -> bool {
self.inner.lock().unwrap().is_playing()
}
}
#[derive(Clone)]
pub struct KeyframeHandle {
inner: SharedKeyframeTrack,
}
pub struct KeyframeBuilder {
points: Vec<(u32, f32)>, default_easing: Easing,
ping_pong: bool,
iterations: i32,
delay_ms: u32,
auto_start: bool,
}
impl KeyframeBuilder {
pub fn new() -> Self {
Self {
points: Vec::new(),
default_easing: Easing::Linear,
ping_pong: false,
iterations: 1,
delay_ms: 0,
auto_start: false,
}
}
pub fn at(mut self, time_ms: u32, value: f32) -> Self {
self.points.push((time_ms, value));
self
}
pub fn ease(mut self, easing: Easing) -> Self {
self.default_easing = easing;
self
}
pub fn ping_pong(mut self) -> Self {
self.ping_pong = true;
self
}
pub fn loop_count(mut self, count: i32) -> Self {
self.iterations = count;
self
}
pub fn loop_infinite(mut self) -> Self {
self.iterations = -1;
self
}
pub fn delay(mut self, delay_ms: u32) -> Self {
self.delay_ms = delay_ms;
self
}
pub fn start(mut self) -> Self {
self.auto_start = true;
self
}
pub(crate) fn build_with_handle(self, handle: SchedulerHandle) -> AnimatedKeyframe {
let duration_ms = self.points.iter().map(|(t, _)| *t).max().unwrap_or(0);
let mut anim = AnimatedKeyframe::new(handle, duration_ms);
for (time_ms, value) in &self.points {
let time = if duration_ms > 0 {
*time_ms as f32 / duration_ms as f32
} else {
0.0
};
anim = anim.keyframe(time, *value, self.default_easing);
}
anim = anim
.iterations(self.iterations)
.ping_pong(self.ping_pong)
.delay(self.delay_ms);
if self.auto_start {
anim = anim.auto_start(true);
}
anim.build()
}
}
impl Default for KeyframeBuilder {
fn default() -> Self {
Self::new()
}
}
impl KeyframeHandle {
pub fn get(&self) -> f32 {
self.inner.lock().unwrap().get()
}
pub fn progress(&self) -> f32 {
self.inner.lock().unwrap().progress()
}
pub fn start(&self) {
self.inner.lock().unwrap().start();
}
pub fn stop(&self) {
self.inner.lock().unwrap().stop();
}
pub fn restart(&self) {
self.inner.lock().unwrap().restart();
}
pub fn is_playing(&self) -> bool {
self.inner.lock().unwrap().is_playing()
}
}
#[allow(clippy::incompatible_msrv)]
static PERSISTED_ANIMATED_VALUES: LazyLock<RwLock<HashMap<String, SharedAnimatedValue>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
#[allow(clippy::incompatible_msrv)]
static PERSISTED_ANIMATED_TIMELINES: LazyLock<RwLock<HashMap<String, SharedAnimatedTimeline>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
#[allow(clippy::incompatible_msrv)]
static PERSISTED_KEYFRAME_TRACKS: LazyLock<RwLock<HashMap<String, SharedKeyframeTrack>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
static NEEDS_REDRAW: AtomicBool = AtomicBool::new(false);
pub fn request_redraw() {
NEEDS_REDRAW.store(true, Ordering::SeqCst);
}
pub fn take_needs_redraw() -> bool {
NEEDS_REDRAW.swap(false, Ordering::SeqCst)
}
pub fn peek_needs_redraw() -> bool {
NEEDS_REDRAW.load(Ordering::SeqCst)
}
#[allow(clippy::incompatible_msrv)]
static PENDING_PROP_UPDATES: LazyLock<Mutex<Vec<(LayoutNodeId, RenderProps)>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
#[allow(clippy::incompatible_msrv)]
static PENDING_SUBTREE_REBUILDS: LazyLock<Mutex<Vec<PendingSubtreeRebuild>>> =
LazyLock::new(|| Mutex::new(Vec::new()));
pub struct PendingSubtreeRebuild {
pub parent_id: LayoutNodeId,
pub new_child: crate::div::Div,
pub needs_layout: bool,
}
unsafe impl Send for PendingSubtreeRebuild {}
pub fn queue_subtree_rebuild(parent_id: LayoutNodeId, new_child: crate::div::Div) {
PENDING_SUBTREE_REBUILDS
.lock()
.unwrap()
.push(PendingSubtreeRebuild {
parent_id,
new_child,
needs_layout: true,
});
}
pub fn queue_visual_subtree_rebuild(parent_id: LayoutNodeId, new_child: crate::div::Div) {
PENDING_SUBTREE_REBUILDS
.lock()
.unwrap()
.push(PendingSubtreeRebuild {
parent_id,
new_child,
needs_layout: false,
});
}
pub fn take_pending_subtree_rebuilds() -> Vec<PendingSubtreeRebuild> {
std::mem::take(&mut *PENDING_SUBTREE_REBUILDS.lock().unwrap())
}
pub fn requeue_subtree_rebuilds(rebuilds: Vec<PendingSubtreeRebuild>) {
PENDING_SUBTREE_REBUILDS.lock().unwrap().extend(rebuilds);
}
pub fn has_pending_subtree_rebuilds() -> bool {
!PENDING_SUBTREE_REBUILDS.lock().unwrap().is_empty()
}
#[allow(clippy::type_complexity, clippy::incompatible_msrv)]
static STATEFUL_BASE_UPDATERS: LazyLock<
Mutex<HashMap<LayoutNodeId, Arc<dyn Fn(RenderProps) + Send + Sync>>>,
> = LazyLock::new(|| Mutex::new(HashMap::new()));
pub(crate) fn register_stateful_base_updater(
node_id: LayoutNodeId,
updater: Arc<dyn Fn(RenderProps) + Send + Sync>,
) {
STATEFUL_BASE_UPDATERS
.lock()
.unwrap()
.insert(node_id, updater);
}
pub fn update_stateful_base_props(node_id: LayoutNodeId, props: RenderProps) {
if let Some(updater) = STATEFUL_BASE_UPDATERS.lock().unwrap().get(&node_id) {
updater(props);
}
}
pub fn has_stateful_base_updater(node_id: LayoutNodeId) -> bool {
STATEFUL_BASE_UPDATERS
.lock()
.unwrap()
.contains_key(&node_id)
}
pub fn clear_stateful_base_updaters() {
STATEFUL_BASE_UPDATERS.lock().unwrap().clear();
}
pub fn clear_stateful_deps() {
STATEFUL_DEPS.lock().unwrap().clear();
}
pub fn clear_stateful_animations() {
STATEFUL_ANIMATIONS.lock().unwrap().clear();
}
#[allow(clippy::type_complexity, clippy::incompatible_msrv)]
static STATEFUL_DEPS: LazyLock<
Mutex<std::collections::HashMap<u64, (Vec<SignalId>, Arc<dyn Fn() + Send + Sync>)>>,
> = LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
pub(crate) fn register_stateful_deps(
stateful_key: u64,
deps: Vec<SignalId>,
refresh_fn: Arc<dyn Fn() + Send + Sync>,
) {
STATEFUL_DEPS
.lock()
.unwrap()
.insert(stateful_key, (deps, refresh_fn));
}
pub fn check_stateful_deps(changed_signals: &[SignalId]) -> bool {
let callbacks_to_call: Vec<Arc<dyn Fn() + Send + Sync>> = {
let registry = STATEFUL_DEPS.lock().unwrap();
registry
.iter()
.filter_map(|(key, (deps, refresh_fn))| {
if deps.iter().any(|d| changed_signals.contains(d)) {
tracing::debug!(
"check_stateful_deps: will trigger refresh for stateful_key={}",
key
);
Some(Arc::clone(refresh_fn))
} else {
None
}
})
.collect()
};
let triggered = !callbacks_to_call.is_empty();
for callback in callbacks_to_call {
callback();
}
triggered
}
#[allow(clippy::type_complexity, clippy::incompatible_msrv)]
static STATEFUL_ANIMATIONS: LazyLock<
Mutex<std::collections::HashMap<u64, (Vec<String>, Arc<dyn Fn() + Send + Sync>)>>,
> = LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
pub(crate) fn register_stateful_animation(
stateful_key: u64,
animation_keys: Vec<String>,
refresh_fn: Arc<dyn Fn() + Send + Sync>,
) {
STATEFUL_ANIMATIONS
.lock()
.unwrap()
.insert(stateful_key, (animation_keys, refresh_fn));
}
#[allow(dead_code)]
pub(crate) fn unregister_stateful_animation(stateful_key: u64) {
STATEFUL_ANIMATIONS.lock().unwrap().remove(&stateful_key);
}
#[allow(clippy::type_complexity)]
pub fn check_stateful_animations() -> bool {
let entries: Vec<(u64, Vec<String>, Arc<dyn Fn() + Send + Sync>)> = {
let registry = STATEFUL_ANIMATIONS.lock().unwrap();
registry
.iter()
.map(|(key, (anim_keys, refresh_fn))| (*key, anim_keys.clone(), Arc::clone(refresh_fn)))
.collect()
};
if entries.is_empty() {
return false;
}
let persisted_values = PERSISTED_ANIMATED_VALUES.read().unwrap();
let persisted_timelines = PERSISTED_ANIMATED_TIMELINES.read().unwrap();
let persisted_keyframes = PERSISTED_KEYFRAME_TRACKS.read().unwrap();
let mut callbacks_to_call = Vec::new();
let mut settled_statefuls = Vec::new();
for (stateful_key, anim_keys, refresh_fn) in entries {
let mut has_active = false;
for anim_key in &anim_keys {
if let Some(animated) = persisted_values.get(anim_key) {
if let Ok(guard) = animated.lock() {
if guard.is_animating() {
has_active = true;
break;
}
}
}
if let Some(timeline) = persisted_timelines.get(anim_key) {
if let Ok(guard) = timeline.lock() {
if guard.is_playing() {
has_active = true;
break;
}
}
}
if let Some(track) = persisted_keyframes.get(anim_key) {
if let Ok(mut guard) = track.lock() {
if guard.is_playing() {
has_active = true;
break;
}
}
}
}
if has_active {
callbacks_to_call.push(refresh_fn);
} else {
settled_statefuls.push(stateful_key);
}
}
drop(persisted_values);
drop(persisted_timelines);
drop(persisted_keyframes);
if !settled_statefuls.is_empty() {
let mut registry = STATEFUL_ANIMATIONS.lock().unwrap();
for key in settled_statefuls {
registry.remove(&key);
}
}
let triggered = !callbacks_to_call.is_empty();
for callback in callbacks_to_call {
callback();
}
triggered
}
pub fn has_animating_statefuls() -> bool {
!STATEFUL_ANIMATIONS.lock().unwrap().is_empty()
}
pub fn take_pending_prop_updates() -> Vec<(LayoutNodeId, RenderProps)> {
std::mem::take(&mut *PENDING_PROP_UPDATES.lock().unwrap())
}
pub fn queue_prop_update(node_id: LayoutNodeId, props: RenderProps) {
PENDING_PROP_UPDATES.lock().unwrap().push((node_id, props));
request_redraw();
}
pub trait StateTransitions:
Clone + Copy + PartialEq + Eq + Hash + Send + Sync + std::fmt::Debug + 'static
{
fn on_event(&self, event: u32) -> Option<Self>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct NoState;
impl StateTransitions for NoState {
fn on_event(&self, _event: u32) -> Option<Self> {
None }
}
pub trait StateId: Clone + Copy + PartialEq + Eq + Hash + Send + Sync + 'static {
fn to_id(&self) -> u32;
fn from_id(id: u32) -> Option<Self>;
}
pub type StateCallback<S> = Arc<dyn Fn(&S, &mut Div) + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ButtonState {
#[default]
Idle,
Hovered,
Pressed,
Disabled,
}
impl StateTransitions for ButtonState {
fn on_event(&self, event: u32) -> Option<Self> {
use blinc_core::events::event_types::*;
match (self, event) {
(ButtonState::Idle, POINTER_ENTER) => Some(ButtonState::Hovered),
(ButtonState::Hovered, POINTER_LEAVE) => Some(ButtonState::Idle),
(ButtonState::Hovered, POINTER_DOWN) => Some(ButtonState::Pressed),
(ButtonState::Idle, POINTER_DOWN) => Some(ButtonState::Pressed),
(ButtonState::Pressed, POINTER_UP) => Some(ButtonState::Hovered),
(ButtonState::Pressed, POINTER_LEAVE) => Some(ButtonState::Idle),
(ButtonState::Disabled, _) => None, _ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ToggleState {
#[default]
Off,
On,
}
impl StateTransitions for ToggleState {
fn on_event(&self, event: u32) -> Option<Self> {
use blinc_core::events::event_types::*;
match (self, event) {
(ToggleState::Off, POINTER_UP) => Some(ToggleState::On),
(ToggleState::On, POINTER_UP) => Some(ToggleState::Off),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum CheckboxState {
#[default]
UncheckedIdle,
UncheckedHovered,
CheckedIdle,
CheckedHovered,
}
impl CheckboxState {
pub fn is_checked(&self) -> bool {
matches!(
self,
CheckboxState::CheckedIdle | CheckboxState::CheckedHovered
)
}
pub fn is_hovered(&self) -> bool {
matches!(
self,
CheckboxState::UncheckedHovered | CheckboxState::CheckedHovered
)
}
}
impl StateTransitions for CheckboxState {
fn on_event(&self, event: u32) -> Option<Self> {
use blinc_core::events::event_types::*;
match (self, event) {
(CheckboxState::UncheckedIdle, POINTER_ENTER) => Some(CheckboxState::UncheckedHovered),
(CheckboxState::UncheckedHovered, POINTER_LEAVE) => Some(CheckboxState::UncheckedIdle),
(CheckboxState::UncheckedHovered, POINTER_UP) => Some(CheckboxState::CheckedHovered),
(CheckboxState::CheckedIdle, POINTER_ENTER) => Some(CheckboxState::CheckedHovered),
(CheckboxState::CheckedHovered, POINTER_LEAVE) => Some(CheckboxState::CheckedIdle),
(CheckboxState::CheckedHovered, POINTER_UP) => Some(CheckboxState::UncheckedHovered),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextFieldState {
#[default]
Idle,
Hovered,
Focused,
FocusedHovered,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ScrollState {
#[default]
Idle,
Scrolling,
Decelerating,
Bouncing,
}
impl ScrollState {
pub fn is_active(&self) -> bool {
!matches!(self, ScrollState::Idle)
}
pub fn is_bouncing(&self) -> bool {
matches!(self, ScrollState::Bouncing)
}
pub fn is_decelerating(&self) -> bool {
matches!(self, ScrollState::Decelerating)
}
}
pub mod scroll_events {
pub const ANIMATION_TICK: u32 = 10000;
pub const SETTLED: u32 = 10001;
pub const HIT_EDGE: u32 = 10002;
}
impl StateTransitions for ScrollState {
fn on_event(&self, event: u32) -> Option<Self> {
use blinc_core::events::event_types::*;
use scroll_events::*;
match (self, event) {
(ScrollState::Idle, SCROLL) => Some(ScrollState::Scrolling),
(ScrollState::Scrolling, SCROLL) => None,
(ScrollState::Scrolling, SCROLL_END) => Some(ScrollState::Decelerating),
(ScrollState::Scrolling, HIT_EDGE) => Some(ScrollState::Bouncing),
(ScrollState::Decelerating, SETTLED) => Some(ScrollState::Idle),
(ScrollState::Decelerating, HIT_EDGE) => Some(ScrollState::Bouncing),
(ScrollState::Decelerating, SCROLL) => Some(ScrollState::Scrolling),
(ScrollState::Bouncing, SETTLED) => Some(ScrollState::Idle),
(ScrollState::Bouncing, SCROLL) => Some(ScrollState::Scrolling),
_ => None,
}
}
}
impl TextFieldState {
pub fn is_focused(&self) -> bool {
matches!(
self,
TextFieldState::Focused | TextFieldState::FocusedHovered
)
}
pub fn is_hovered(&self) -> bool {
matches!(
self,
TextFieldState::Hovered | TextFieldState::FocusedHovered
)
}
}
impl StateTransitions for TextFieldState {
fn on_event(&self, event: u32) -> Option<Self> {
use blinc_core::events::event_types::*;
match (self, event) {
(TextFieldState::Idle, POINTER_ENTER) => Some(TextFieldState::Hovered),
(TextFieldState::Idle, FOCUS) => Some(TextFieldState::Focused),
(TextFieldState::Idle, POINTER_DOWN) => Some(TextFieldState::Focused),
(TextFieldState::Hovered, POINTER_LEAVE) => Some(TextFieldState::Idle),
(TextFieldState::Hovered, POINTER_DOWN) => Some(TextFieldState::Focused),
(TextFieldState::Hovered, FOCUS) => Some(TextFieldState::FocusedHovered),
(TextFieldState::Focused, BLUR) => Some(TextFieldState::Idle),
(TextFieldState::Focused, POINTER_ENTER) => Some(TextFieldState::FocusedHovered),
(TextFieldState::FocusedHovered, POINTER_LEAVE) => Some(TextFieldState::Focused),
(TextFieldState::FocusedHovered, BLUR) => Some(TextFieldState::Hovered),
(TextFieldState::Disabled, _) => None,
_ => None,
}
}
}
impl StateTransitions for () {
fn on_event(&self, _event: u32) -> Option<Self> {
None }
}
pub struct Stateful<S: StateTransitions> {
inner: RefCell<Div>,
shared_state: Arc<Mutex<StatefulInner<S>>>,
children_cache: RefCell<Vec<Box<dyn ElementBuilder>>>,
event_handlers_cache: RefCell<crate::event_handler::EventHandlers>,
layout_bounds: crate::renderer::LayoutBoundsStorage,
layout_bounds_cb: Option<crate::renderer::LayoutBoundsCallback>,
}
pub struct StatefulInner<S: StateTransitions> {
pub state: S,
pub(crate) state_callback: Option<StateCallback<S>>,
pub(crate) needs_visual_update: bool,
pub(crate) base_render_props: Option<RenderProps>,
pub(crate) base_style: Option<taffy::Style>,
pub(crate) node_id: Option<LayoutNodeId>,
pub(crate) deps: Vec<SignalId>,
pub(crate) ancestor_motion_key: Option<String>,
pub(crate) current_event: Option<crate::event_handler::EventContext>,
pub(crate) refresh_callback: Option<Arc<dyn Fn() + Send + Sync>>,
pub(crate) animation_keys: Vec<String>,
pub(crate) previous_structural_hash: Option<crate::diff::DivHash>,
pub(crate) base_classes: Vec<String>,
pub(crate) base_element_id: Option<String>,
}
impl<S: StateTransitions> StatefulInner<S> {
pub fn new(state: S) -> Self {
Self {
state,
state_callback: None,
needs_visual_update: false,
base_render_props: None,
base_style: None,
node_id: None,
deps: Vec::new(),
ancestor_motion_key: None,
current_event: None,
refresh_callback: None,
animation_keys: Vec::new(),
previous_structural_hash: None,
base_classes: Vec::new(),
base_element_id: None,
}
}
}
impl<S: StateTransitions + Default> Default for Stateful<S> {
fn default() -> Self {
Self::new(S::default())
}
}
pub type SharedState<S> = Arc<Mutex<StatefulInner<S>>>;
pub fn use_shared_state<S>(key: &str) -> SharedState<S>
where
S: StateTransitions + Default + Clone + Send + Sync + 'static,
{
use blinc_core::context_state::BlincContextState;
let ctx = BlincContextState::get();
let state: blinc_core::State<Option<SharedState<S>>> = ctx.use_state_keyed(key, || None);
let existing = state.get();
if let Some(shared) = existing {
shared
} else {
let shared: SharedState<S> = Arc::new(Mutex::new(StatefulInner::new(S::default())));
state.set(Some(shared.clone()));
shared
}
}
pub fn use_shared_state_with<S>(key: &str, initial: S) -> SharedState<S>
where
S: StateTransitions + Clone + Send + Sync + 'static,
{
use blinc_core::context_state::BlincContextState;
let ctx = BlincContextState::get();
let state: blinc_core::State<Option<SharedState<S>>> = ctx.use_state_keyed(key, || None);
let existing = state.get();
if let Some(shared) = existing {
shared
} else {
let shared: SharedState<S> = Arc::new(Mutex::new(StatefulInner::new(initial)));
state.set(Some(shared.clone()));
shared
}
}
#[derive(Default)]
pub struct ChildKeyCounter {
counters: std::collections::HashMap<&'static str, usize>,
path: Vec<String>,
}
impl ChildKeyCounter {
pub fn new() -> Self {
Self {
counters: std::collections::HashMap::new(),
path: Vec::new(),
}
}
pub fn next(&mut self, element_type: &'static str) -> usize {
let index = self.counters.entry(element_type).or_insert(0);
let current = *index;
*index += 1;
current
}
pub fn reset(&mut self) {
self.counters.clear();
self.path.clear();
}
pub fn push(&mut self, segment: String) {
self.path.push(segment);
}
pub fn pop(&mut self) {
self.path.pop();
}
pub fn path_string(&self) -> String {
self.path.join("->")
}
}
#[derive(Clone)]
#[allow(clippy::arc_with_non_send_sync)]
pub struct StateContext<S: StateTransitions> {
state: S,
key: Arc<String>,
child_counter: Arc<RefCell<ChildKeyCounter>>,
reactive: blinc_core::context_state::SharedReactiveGraph,
shared_state: SharedState<S>,
parent_key: Option<Arc<String>>,
deps: Vec<blinc_core::SignalId>,
event: Option<crate::event_handler::EventContext>,
animation_keys: Arc<RefCell<Vec<String>>>,
timeline_refs: Arc<RefCell<Vec<(String, SharedAnimatedTimeline)>>>,
keyframe_refs: Arc<RefCell<Vec<(String, SharedKeyframeTrack)>>>,
}
impl<S: StateTransitions> StateContext<S> {
#[allow(clippy::arc_with_non_send_sync)]
pub(crate) fn new(
state: S,
key: String,
reactive: blinc_core::context_state::SharedReactiveGraph,
shared_state: SharedState<S>,
parent_key: Option<Arc<String>>,
deps: Vec<blinc_core::SignalId>,
event: Option<crate::event_handler::EventContext>,
) -> Self {
Self {
state,
key: Arc::new(key.clone()),
child_counter: Arc::new(RefCell::new(ChildKeyCounter::new())),
reactive,
shared_state,
parent_key,
deps,
event,
animation_keys: Arc::new(RefCell::new(Vec::new())),
timeline_refs: Arc::new(RefCell::new(Vec::new())),
keyframe_refs: Arc::new(RefCell::new(Vec::new())),
}
}
pub(crate) fn take_animation_keys(&self) -> Vec<String> {
let mut keys = std::mem::take(&mut *self.animation_keys.borrow_mut());
let timeline_refs = self.timeline_refs.borrow();
for (key, timeline) in timeline_refs.iter() {
if timeline.lock().unwrap().is_playing() {
keys.push(key.clone());
}
}
let keyframe_refs = self.keyframe_refs.borrow();
for (key, track) in keyframe_refs.iter() {
if track.lock().unwrap().is_playing() {
keys.push(key.clone());
}
}
keys
}
fn track_animation_key(&self, key: String) {
let mut keys = self.animation_keys.borrow_mut();
if !keys.contains(&key) {
keys.push(key);
}
}
pub fn state(&self) -> S {
self.state
}
pub fn event(&self) -> Option<&crate::event_handler::EventContext> {
self.event.as_ref()
}
pub fn key(&self) -> &str {
&self.key
}
pub fn full_key(&self) -> String {
match &self.parent_key {
Some(parent) => format!("{}:{}", parent, self.key),
None => self.key.to_string(),
}
}
pub fn dispatch(&self, event: u32) {
let mut inner = self.shared_state.lock().unwrap();
if let Some(new_state) = inner.state.on_event(event) {
inner.state = new_state;
inner.needs_visual_update = true;
drop(inner);
request_redraw();
}
}
pub fn use_signal<T, F>(&self, name: &str, init: F) -> blinc_core::State<T>
where
T: Clone + Send + 'static,
F: FnOnce() -> T,
{
let signal_key = format!("{}:signal:{}", self.full_key(), name);
let state = blinc_core::context_state::use_state_keyed(&signal_key, init);
self.use_effect(&state);
state
}
pub fn derive_child_key(&self, element_type: &'static str) -> String {
let mut counter = self.child_counter.borrow_mut();
let index = counter.next(element_type);
let path = counter.path_string();
if path.is_empty() {
format!("{}:{}:{}", self.full_key(), element_type, index)
} else {
format!("{}:{}->{}:{}", self.full_key(), path, element_type, index)
}
}
pub fn push_hierarchy(&self, segment: &str) {
self.child_counter.borrow_mut().push(segment.to_string());
}
pub fn pop_hierarchy(&self) {
self.child_counter.borrow_mut().pop();
}
pub(crate) fn reset_counter(&self) {
self.child_counter.borrow_mut().reset();
}
pub(crate) fn shared_state(&self) -> &SharedState<S> {
&self.shared_state
}
pub(crate) fn reactive(&self) -> &blinc_core::context_state::SharedReactiveGraph {
&self.reactive
}
pub fn use_animated_value(&self, name: &str, initial: f32) -> SharedAnimatedValue {
self.use_animated_value_with_config(name, initial, SpringConfig::stiff())
}
pub fn use_animated_value_with_config(
&self,
name: &str,
initial: f32,
config: SpringConfig,
) -> SharedAnimatedValue {
let anim_key = format!("{}:anim:{}", self.full_key(), name);
{
let values = PERSISTED_ANIMATED_VALUES.read().unwrap();
if let Some(existing) = values.get(&anim_key) {
return Arc::clone(existing);
}
}
let handle = blinc_animation::get_scheduler();
let animated = AnimatedValue::new(handle, initial, config);
let shared = Arc::new(Mutex::new(animated));
{
let mut values = PERSISTED_ANIMATED_VALUES.write().unwrap();
values.insert(anim_key, Arc::clone(&shared));
}
shared
}
pub fn use_spring(&self, name: &str, target: f32, config: SpringConfig) -> f32 {
let anim_key = format!("{}:anim:{}", self.full_key(), name);
let animated = self.use_animated_value_with_config(name, target, config);
let mut guard = animated.lock().unwrap();
guard.set_target(target);
let value = guard.get();
if guard.is_animating() {
self.track_animation_key(anim_key);
}
value
}
pub fn spring(&self, name: &str, target: f32) -> f32 {
self.use_spring(name, target, SpringConfig::stiff())
}
pub fn use_timeline<T, F>(&self, name: &str, configure: F) -> (T, TimelineHandle)
where
F: FnOnce(&mut AnimatedTimeline) -> T,
T: blinc_animation::ConfigureResult,
{
let timeline_key = format!("{}:timeline:{}", self.full_key(), name);
let shared = {
let timelines = PERSISTED_ANIMATED_TIMELINES.read().unwrap();
if let Some(existing) = timelines.get(&timeline_key) {
Arc::clone(existing)
} else {
drop(timelines);
let handle = blinc_animation::get_scheduler();
let timeline = AnimatedTimeline::new(handle);
let shared = Arc::new(Mutex::new(timeline));
let mut timelines = PERSISTED_ANIMATED_TIMELINES.write().unwrap();
timelines.insert(timeline_key.clone(), Arc::clone(&shared));
shared
}
};
let result = {
let mut tl = shared.lock().unwrap();
tl.configure(configure)
};
{
let mut refs = self.timeline_refs.borrow_mut();
if !refs.iter().any(|(k, _)| k == &timeline_key) {
refs.push((timeline_key, Arc::clone(&shared)));
}
}
(result, TimelineHandle { inner: shared })
}
pub fn use_keyframes<F>(&self, name: &str, configure: F) -> KeyframeHandle
where
F: FnOnce(KeyframeBuilder) -> KeyframeBuilder,
{
let keyframe_key = format!("{}:keyframes:{}", self.full_key(), name);
let shared = {
let tracks = PERSISTED_KEYFRAME_TRACKS.read().unwrap();
if let Some(existing) = tracks.get(&keyframe_key) {
Arc::clone(existing)
} else {
drop(tracks);
let builder = KeyframeBuilder::new();
let configured = configure(builder);
let handle = blinc_animation::get_scheduler();
let anim = configured.build_with_handle(handle);
let shared = Arc::new(Mutex::new(anim));
let mut tracks = PERSISTED_KEYFRAME_TRACKS.write().unwrap();
tracks.insert(keyframe_key.clone(), Arc::clone(&shared));
shared
}
};
{
let mut refs = self.keyframe_refs.borrow_mut();
if !refs.iter().any(|(k, _)| k == &keyframe_key) {
refs.push((keyframe_key, Arc::clone(&shared)));
}
}
KeyframeHandle { inner: shared }
}
pub fn dep<T: Clone + Send + Default + 'static>(&self, index: usize) -> Option<T> {
let signal_id = self.deps.get(index)?;
let signal = blinc_core::Signal::from_id(*signal_id);
self.reactive.lock().unwrap().get(signal)
}
pub fn dep_as_state<T: Clone + Send + 'static>(
&self,
index: usize,
) -> Option<blinc_core::State<T>> {
let signal_id = self.deps.get(index)?;
let signal = blinc_core::Signal::from_id(*signal_id);
let dirty_flag = blinc_core::context_state::BlincContextState::get()
.dirty_flag()
.clone();
Some(blinc_core::State::new(
signal,
self.reactive.clone(),
dirty_flag,
))
}
pub fn dep_signal_id(&self, index: usize) -> Option<blinc_core::SignalId> {
self.deps.get(index).copied()
}
pub fn use_effect<T: Clone + Send + 'static>(&self, signal: &blinc_core::State<T>) {
let signal_id = signal.signal_id();
let mut inner = self.shared_state.lock().unwrap();
if inner.deps.contains(&signal_id) {
return;
}
inner.deps.push(signal_id);
tracing::info!(
"use_effect: registered signal {:?}, total deps: {}",
signal_id,
inner.deps.len()
);
let refresh_callback = inner.refresh_callback.clone();
let deps = inner.deps.clone();
drop(inner);
if let Some(callback) = refresh_callback {
let stateful_key = Arc::as_ptr(&self.shared_state) as u64;
tracing::debug!(
"use_effect: re-registering deps for stateful_key={}",
stateful_key
);
register_stateful_deps(stateful_key, deps, Arc::new(move || callback()));
} else {
tracing::warn!("use_effect: no refresh_callback available!");
}
}
pub fn query_motion(&self, name: &str) -> crate::selector::MotionHandle {
let motion_key = format!("motion:{}:motion:{}:child:0", self.full_key(), name);
crate::selector::query_motion(&motion_key)
}
pub fn motion(&self, name: &str) -> crate::motion::Motion {
let motion_key = format!("{}:motion:{}", self.full_key(), name);
crate::motion::motion_derived(&motion_key)
}
}
fn assign_inner_ids_recursive<S: StateTransitions>(
ctx: &StateContext<S>,
element: &mut dyn crate::div::ElementBuilder,
) {
let children = element.children_builders_mut();
for child in children.iter_mut() {
let type_name = match child.element_type_id() {
crate::div::ElementTypeId::Text => "text",
crate::div::ElementTypeId::StyledText => "styled_text",
crate::div::ElementTypeId::Image => "img",
crate::div::ElementTypeId::Svg => "svg",
crate::div::ElementTypeId::Canvas => "canvas",
crate::div::ElementTypeId::Motion => "motion",
_ => "div",
};
let auto_id = ctx.derive_child_key(type_name);
child.set_auto_id(auto_id);
assign_inner_ids_recursive(ctx, child.as_mut());
}
}
pub type StateContextCallback<S> =
Arc<dyn Fn(&StateContext<S>) -> crate::div::Div + Send + Sync + 'static>;
pub struct StatefulBuilder<S: StateTransitions> {
key: crate::InstanceKey,
deps: Vec<blinc_core::reactive::SignalId>,
initial_state: Option<S>,
parent_key: Option<Arc<String>>,
}
impl<S: StateTransitions + Default> StatefulBuilder<S> {
#[track_caller]
pub fn new() -> Self {
Self {
key: crate::InstanceKey::new("stateful"),
deps: Vec::new(),
initial_state: None,
parent_key: None,
}
}
pub fn deps(mut self, deps: impl IntoIterator<Item = blinc_core::reactive::SignalId>) -> Self {
self.deps = deps.into_iter().collect();
self
}
pub fn initial(mut self, state: S) -> Self {
self.initial_state = Some(state);
self
}
pub fn parent_key(mut self, key: Arc<String>) -> Self {
self.parent_key = Some(key);
self
}
pub fn on_state<F>(self, callback: F) -> Stateful<S>
where
F: Fn(&StateContext<S>) -> crate::div::Div + Send + Sync + 'static,
{
let initial = self.initial_state.unwrap_or_default();
let key_str = self.key.get().to_string();
let parent_key = self.parent_key;
let deps = self.deps;
let shared_state = use_shared_state_with::<S>(&key_str, initial);
let reactive = blinc_core::context_state::BlincContextState::get()
.reactive()
.clone();
let callback = Arc::new(callback);
let callback_clone = callback.clone();
let key_str_clone = key_str.clone();
let parent_key_clone = parent_key.clone();
let reactive_clone = reactive.clone();
let shared_state_clone = shared_state.clone();
let deps_clone = deps.clone();
let legacy_callback: StateCallback<S> =
Arc::new(move |state: &S, div: &mut crate::div::Div| {
let current_event = shared_state_clone.lock().unwrap().current_event.clone();
let ctx = StateContext::new(
*state, key_str_clone.clone(),
reactive_clone.clone(),
shared_state_clone.clone(),
parent_key_clone.clone(),
deps_clone.clone(),
current_event,
);
ctx.reset_counter();
let mut user_div = callback_clone(&ctx);
assign_inner_ids_recursive(&ctx, &mut user_div);
let user_div = user_div.with_stateful_context(ctx.full_key());
div.merge(user_div);
let anim_keys = ctx.take_animation_keys();
if !anim_keys.is_empty() {
shared_state_clone.lock().unwrap().animation_keys = anim_keys;
}
});
let mut stateful = Stateful::with_shared_state(shared_state);
stateful.shared_state.lock().unwrap().state_callback = Some(legacy_callback);
stateful.shared_state.lock().unwrap().needs_visual_update = true;
if !deps.is_empty() {
stateful.shared_state.lock().unwrap().deps = deps.clone();
}
let stateful = stateful.register_state_handlers();
let shared = Arc::clone(&stateful.shared_state);
let shared_for_refresh = Arc::clone(&shared);
let refresh_callback: Arc<dyn Fn() + Send + Sync> = Arc::new(move || {
refresh_stateful(&shared_for_refresh);
});
stateful.shared_state.lock().unwrap().refresh_callback =
Some(Arc::clone(&refresh_callback));
stateful.apply_state_callback();
let current_deps = stateful.shared_state.lock().unwrap().deps.clone();
let stateful_key = Arc::as_ptr(&shared) as u64;
if !current_deps.is_empty() {
register_stateful_deps(
stateful_key,
current_deps,
Arc::new({
let refresh_callback = Arc::clone(&refresh_callback);
move || refresh_callback()
}),
);
}
let anim_keys = stateful.shared_state.lock().unwrap().animation_keys.clone();
if !anim_keys.is_empty() {
register_stateful_animation(stateful_key, anim_keys, Arc::clone(&refresh_callback));
}
stateful
}
}
impl<S: StateTransitions + Default> Default for StatefulBuilder<S> {
fn default() -> Self {
Self::new()
}
}
#[track_caller]
pub fn stateful<S: StateTransitions + Default>() -> StatefulBuilder<S> {
StatefulBuilder::new()
}
pub fn stateful_with_key<S: StateTransitions + Default>(
key: impl Into<String>,
) -> StatefulBuilder<S> {
StatefulBuilder {
key: crate::InstanceKey::explicit(key),
deps: Vec::new(),
initial_state: None,
parent_key: None,
}
}
pub(crate) fn refresh_stateful<S: StateTransitions>(shared: &SharedState<S>) {
Stateful::<S>::refresh_props_internal(shared);
}
impl<S: StateTransitions> Stateful<S> {
fn ensure_callback_invoked(&self) {
let shared = self.shared_state.lock().unwrap();
let has_callback = shared.state_callback.is_some();
let needs_update = shared.needs_visual_update;
tracing::trace!(
"ensure_callback_invoked: has_callback={}, needs_update={}",
has_callback,
needs_update
);
if needs_update && has_callback {
let callback = Arc::clone(shared.state_callback.as_ref().unwrap());
let state_copy = shared.state;
drop(shared);
tracing::trace!("Invoking state callback for Stateful");
callback(&state_copy, &mut self.inner.borrow_mut());
self.shared_state.lock().unwrap().needs_visual_update = false;
{
let inner = self.inner.borrow();
if !inner.event_handlers.is_empty() {
let handlers_clone = inner.event_handlers.clone();
drop(inner);
self.event_handlers_cache.borrow_mut().merge(handlers_clone);
}
}
let children_count = self.inner.borrow().children.len();
tracing::trace!("After callback: {} children in inner Div", children_count);
}
}
pub fn new(initial_state: S) -> Self {
Self {
inner: RefCell::new(Div::new()),
shared_state: Arc::new(Mutex::new(StatefulInner {
state: initial_state,
state_callback: None,
needs_visual_update: false,
base_render_props: None,
base_style: None,
node_id: None,
deps: Vec::new(),
ancestor_motion_key: None,
current_event: None,
refresh_callback: None,
animation_keys: Vec::new(),
previous_structural_hash: None,
base_classes: Vec::new(),
base_element_id: None,
})),
children_cache: RefCell::new(Vec::new()),
event_handlers_cache: RefCell::new(crate::event_handler::EventHandlers::new()),
layout_bounds: Arc::new(std::sync::Mutex::new(None)),
layout_bounds_cb: None,
}
}
pub fn with_shared_state(shared_state: SharedState<S>) -> Self {
Self {
inner: RefCell::new(Div::new()),
shared_state,
children_cache: RefCell::new(Vec::new()),
event_handlers_cache: RefCell::new(crate::event_handler::EventHandlers::new()),
layout_bounds: Arc::new(std::sync::Mutex::new(None)),
layout_bounds_cb: None,
}
}
pub fn shared_state(&self) -> SharedState<S> {
Arc::clone(&self.shared_state)
}
pub fn default_state(self, state: S) -> Self {
self.shared_state.lock().unwrap().state = state;
self
}
pub fn state(&self) -> S {
self.shared_state.lock().unwrap().state
}
pub fn inner_render_props(&self) -> RenderProps {
self.inner.borrow().render_props()
}
pub fn inner_layout_style(&self) -> Option<taffy::Style> {
self.inner.borrow().layout_style().cloned()
}
pub fn inner_scroll_physics(&self) -> Option<crate::scroll::SharedScrollPhysics> {
self.inner.borrow().scroll_physics.clone()
}
pub fn apply_callback(&self) {
self.apply_state_callback();
}
pub fn set_state(&self, state: S) {
let mut inner = self.shared_state.lock().unwrap();
inner.state = state;
}
pub fn on_state<F>(self, callback: F) -> Self
where
F: Fn(&S, &mut Div) + Send + Sync + 'static,
{
let inner_div = self.inner.borrow();
let base_props = inner_div.render_props();
let base_style = inner_div.layout_style().cloned();
drop(inner_div);
{
let mut inner = self.shared_state.lock().unwrap();
inner.state_callback = Some(Arc::new(callback));
inner.base_render_props = Some(base_props);
inner.base_style = base_style;
}
let s = self.register_state_handlers();
s.apply_state_callback();
let shared = Arc::clone(&s.shared_state);
let deps = shared.lock().unwrap().deps.clone();
if !deps.is_empty() {
let stateful_key = Arc::as_ptr(&shared) as u64;
let shared_for_refresh = Arc::clone(&shared);
register_stateful_deps(
stateful_key,
deps,
Arc::new(move || {
refresh_stateful(&shared_for_refresh);
}),
);
}
s
}
pub fn deps(self, signals: &[SignalId]) -> Self {
{
let mut inner = self.shared_state.lock().unwrap();
inner.deps = signals.to_vec();
}
self
}
fn register_state_handlers(self) -> Self {
self.ensure_state_handlers_registered();
self
}
pub fn ensure_state_handlers_registered(&self) {
use blinc_core::events::event_types;
let shared = Arc::clone(&self.shared_state);
let mut cache = self.event_handlers_cache.borrow_mut();
if cache.has_handler(event_types::POINTER_ENTER) {
return; }
{
let shared_clone = Arc::clone(&shared);
cache.on_hover_enter(move |ctx| {
Self::handle_event_internal(
&shared_clone,
event_types::POINTER_ENTER,
Some(ctx.clone()),
);
});
}
{
let shared_clone = Arc::clone(&shared);
cache.on_hover_leave(move |ctx| {
Self::handle_event_internal(
&shared_clone,
event_types::POINTER_LEAVE,
Some(ctx.clone()),
);
});
}
{
let shared_clone = Arc::clone(&shared);
cache.on_mouse_down(move |ctx| {
Self::handle_event_internal(
&shared_clone,
event_types::POINTER_DOWN,
Some(ctx.clone()),
);
});
}
{
let shared_clone = Arc::clone(&shared);
cache.on_mouse_up(move |ctx| {
Self::handle_event_internal(
&shared_clone,
event_types::POINTER_UP,
Some(ctx.clone()),
);
});
}
{
let shared_clone = Arc::clone(&shared);
cache.on_drag(move |ctx| {
Self::handle_event_internal(&shared_clone, event_types::DRAG, Some(ctx.clone()));
});
}
{
let shared_clone = Arc::clone(&shared);
cache.on_drag_end(move |ctx| {
Self::handle_event_internal(
&shared_clone,
event_types::DRAG_END,
Some(ctx.clone()),
);
});
}
}
fn handle_event_internal(
shared: &Arc<Mutex<StatefulInner<S>>>,
event: u32,
event_context: Option<crate::event_handler::EventContext>,
) {
let mut guard = shared.lock().unwrap();
if guard.node_id.is_none() {
if let Some(ref ctx) = event_context {
guard.node_id = Some(ctx.node_id);
}
}
if let Some(ref motion_key) = guard.ancestor_motion_key {
let motion_state = blinc_core::query_motion(motion_key);
if motion_state.is_animating() {
tracing::trace!(
"Suppressing state transition: ancestor motion '{}' is animating",
motion_key
);
return;
}
}
let new_state = match guard.state.on_event(event) {
Some(s) if s != guard.state => s,
_ => return,
};
guard.state = new_state;
guard.needs_visual_update = true;
guard.current_event = event_context;
if let Some(ref callback) = guard.state_callback {
let callback = Arc::clone(callback);
let state_copy = guard.state;
let cached_node_id = guard.node_id;
let base_props = guard.base_render_props.clone();
let base_style = guard.base_style.clone();
drop(guard);
let mut temp_div = if let Some(style) = base_style {
Div::with_style(style)
} else {
Div::new()
};
callback(&state_copy, &mut temp_div);
let callback_props = temp_div.render_props();
let mut final_props = base_props.unwrap_or_default();
final_props.merge_from(&callback_props);
if let Some(nid) = cached_node_id {
queue_prop_update(nid, final_props);
if !temp_div.children_builders().is_empty() {
queue_visual_subtree_rebuild(nid, temp_div);
}
}
let mut inner = shared.lock().unwrap();
inner.current_event = None;
let anim_keys = inner.animation_keys.clone();
let refresh_cb = inner.refresh_callback.clone();
drop(inner);
if !anim_keys.is_empty() {
if let Some(refresh_cb) = refresh_cb {
let stateful_key = Arc::as_ptr(shared) as u64;
register_stateful_animation(stateful_key, anim_keys, refresh_cb);
}
}
}
request_redraw();
}
fn refresh_props_internal(shared: &Arc<Mutex<StatefulInner<S>>>) {
let mut guard = shared.lock().unwrap();
guard.current_event = None;
let (
callback,
state_copy,
cached_node_id,
base_props,
base_style,
refresh_callback,
base_classes,
base_element_id,
) = match (guard.state_callback.as_ref(), guard.node_id) {
(Some(cb), Some(nid)) => {
let callback = Arc::clone(cb);
let state = guard.state;
let base = guard.base_render_props.clone();
let style = guard.base_style.clone();
let refresh_cb = guard.refresh_callback.clone();
let classes = guard.base_classes.clone();
let elt_id = guard.base_element_id.clone();
drop(guard);
(
callback, state, nid, base, style, refresh_cb, classes, elt_id,
)
}
_ => return,
};
let base_style_clone = base_style.clone();
let mut temp_div = if let Some(style) = base_style {
Div::with_style(style)
} else {
Div::new()
};
callback(&state_copy, &mut temp_div);
for cls in &base_classes {
if !temp_div.classes.contains(cls) {
temp_div.classes.push(cls.clone());
}
}
if temp_div.element_id.is_none() {
if let Some(id) = base_element_id {
temp_div.element_id = Some(id);
}
}
let callback_props = temp_div.render_props();
let mut final_props = base_props.unwrap_or_default();
final_props.merge_from(&callback_props);
queue_prop_update(cached_node_id, final_props);
let children = temp_div.children_builders();
let style_changed = temp_div.layout_style() != base_style_clone.as_ref();
if !children.is_empty() || style_changed {
let new_structural_hash = crate::diff::DivHash::compute_structural_tree(
&temp_div as &dyn crate::div::ElementBuilder,
);
let prev_hash = shared.lock().unwrap().previous_structural_hash;
let structure_changed = prev_hash != Some(new_structural_hash);
shared.lock().unwrap().previous_structural_hash = Some(new_structural_hash);
if structure_changed {
queue_subtree_rebuild(cached_node_id, temp_div);
} else {
queue_visual_subtree_rebuild(cached_node_id, temp_div);
}
}
let anim_keys = shared.lock().unwrap().animation_keys.clone();
if !anim_keys.is_empty() {
if let Some(refresh_cb) = refresh_callback {
let stateful_key = Arc::as_ptr(shared) as u64;
register_stateful_animation(stateful_key, anim_keys, refresh_cb);
}
}
request_redraw();
}
pub fn dispatch_state(&self, new_state: S) -> bool {
let mut shared = self.shared_state.lock().unwrap();
if shared.state != new_state {
shared.state = new_state;
drop(shared);
Self::refresh_props_internal(&self.shared_state);
true
} else {
false
}
}
pub fn handle_event(&self, event: u32) -> bool {
let new_state = {
let inner = self.shared_state.lock().unwrap();
inner.state.on_event(event)
};
if let Some(new_state) = new_state {
self.dispatch_state(new_state)
} else {
false
}
}
fn apply_state_callback(&self) {
let mut shared = self.shared_state.lock().unwrap();
if let Some(ref callback) = shared.state_callback {
let callback = Arc::clone(callback);
let state_copy = shared.state;
shared.needs_visual_update = false;
drop(shared); callback(&state_copy, &mut self.inner.borrow_mut());
{
let inner = self.inner.borrow();
if !inner.event_handlers.is_empty() {
let handlers_clone = inner.event_handlers.clone();
drop(inner);
self.event_handlers_cache.borrow_mut().merge(handlers_clone);
}
}
}
}
pub fn id(self, id: &str) -> Self {
self.merge_into_inner(Div::new().id(id));
self.shared_state.lock().unwrap().base_element_id = Some(id.to_string());
self
}
pub fn class(self, name: &str) -> Self {
self.inner.borrow_mut().classes.push(name.to_string());
self.shared_state
.lock()
.unwrap()
.base_classes
.push(name.to_string());
self
}
fn merge_into_inner(&self, props: Div) {
self.inner.borrow_mut().merge(props);
}
pub fn w(self, px: f32) -> Self {
self.merge_into_inner(Div::new().w(px));
self
}
pub fn h(self, px: f32) -> Self {
self.merge_into_inner(Div::new().h(px));
self
}
pub fn w_full(self) -> Self {
self.merge_into_inner(Div::new().w_full());
self
}
pub fn min_w(self, px: f32) -> Self {
self.merge_into_inner(Div::new().min_w(px));
self
}
pub fn h_full(self) -> Self {
self.merge_into_inner(Div::new().h_full());
self
}
pub fn size(self, w: f32, h: f32) -> Self {
self.merge_into_inner(Div::new().size(w, h));
self
}
pub fn square(self, size: f32) -> Self {
self.merge_into_inner(Div::new().square(size));
self
}
pub fn flex_row(self) -> Self {
self.merge_into_inner(Div::new().flex_row());
self
}
pub fn flex_col(self) -> Self {
self.merge_into_inner(Div::new().flex_col());
self
}
pub fn flex_grow(self) -> Self {
self.merge_into_inner(Div::new().flex_grow());
self
}
pub fn w_fit(self) -> Self {
self.merge_into_inner(Div::new().w_fit());
self
}
pub fn h_fit(self) -> Self {
self.merge_into_inner(Div::new().h_fit());
self
}
pub fn p(self, units: f32) -> Self {
self.merge_into_inner(Div::new().p(units));
self
}
pub fn px(self, units: f32) -> Self {
self.merge_into_inner(Div::new().px(units));
self
}
pub fn py(self, units: f32) -> Self {
self.merge_into_inner(Div::new().py(units));
self
}
pub fn padding(self, len: crate::units::Length) -> Self {
self.merge_into_inner(Div::new().padding(len));
self
}
pub fn padding_x(self, len: crate::units::Length) -> Self {
self.merge_into_inner(Div::new().padding_x(len));
self
}
pub fn padding_y(self, len: crate::units::Length) -> Self {
self.merge_into_inner(Div::new().padding_y(len));
self
}
pub fn pt(self, units: f32) -> Self {
self.merge_into_inner(Div::new().pt(units));
self
}
pub fn pb(self, units: f32) -> Self {
self.merge_into_inner(Div::new().pb(units));
self
}
pub fn pl(self, units: f32) -> Self {
self.merge_into_inner(Div::new().pl(units));
self
}
pub fn pr(self, units: f32) -> Self {
self.merge_into_inner(Div::new().pr(units));
self
}
pub fn mt(self, units: f32) -> Self {
self.merge_into_inner(Div::new().mt(units));
self
}
pub fn mb(self, units: f32) -> Self {
self.merge_into_inner(Div::new().mb(units));
self
}
pub fn ml(self, units: f32) -> Self {
self.merge_into_inner(Div::new().ml(units));
self
}
pub fn mr(self, units: f32) -> Self {
self.merge_into_inner(Div::new().mr(units));
self
}
pub fn mx(self, units: f32) -> Self {
self.merge_into_inner(Div::new().mx(units));
self
}
pub fn my(self, units: f32) -> Self {
self.merge_into_inner(Div::new().my(units));
self
}
pub fn m(self, units: f32) -> Self {
self.merge_into_inner(Div::new().m(units));
self
}
pub fn gap(self, units: f32) -> Self {
self.merge_into_inner(Div::new().gap(units));
self
}
pub fn items_start(self) -> Self {
self.merge_into_inner(Div::new().items_start());
self
}
pub fn items_center(self) -> Self {
self.merge_into_inner(Div::new().items_center());
self
}
pub fn items_end(self) -> Self {
self.merge_into_inner(Div::new().items_end());
self
}
pub fn justify_start(self) -> Self {
self.merge_into_inner(Div::new().justify_start());
self
}
pub fn justify_center(self) -> Self {
self.merge_into_inner(Div::new().justify_center());
self
}
pub fn justify_end(self) -> Self {
self.merge_into_inner(Div::new().justify_end());
self
}
pub fn justify_between(self) -> Self {
self.merge_into_inner(Div::new().justify_between());
self
}
pub fn bg(self, color: impl Into<blinc_core::Brush>) -> Self {
self.merge_into_inner(Div::new().background(color));
self
}
pub fn rounded(self, radius: f32) -> Self {
self.merge_into_inner(Div::new().rounded(radius));
self
}
pub fn border(self, width: f32, color: blinc_core::Color) -> Self {
self.merge_into_inner(Div::new().border(width, color));
self
}
pub fn border_color(self, color: blinc_core::Color) -> Self {
self.merge_into_inner(Div::new().border_color(color));
self
}
pub fn border_width(self, width: f32) -> Self {
self.merge_into_inner(Div::new().border_width(width));
self
}
pub fn shadow(self, shadow: blinc_core::Shadow) -> Self {
self.merge_into_inner(Div::new().shadow(shadow));
self
}
pub fn shadow_sm(self) -> Self {
self.merge_into_inner(Div::new().shadow_sm());
self
}
pub fn shadow_md(self) -> Self {
self.merge_into_inner(Div::new().shadow_md());
self
}
pub fn shadow_lg(self) -> Self {
self.merge_into_inner(Div::new().shadow_lg());
self
}
pub fn shadow_xl(self) -> Self {
self.merge_into_inner(Div::new().shadow_xl());
self
}
pub fn opacity(self, opacity: f32) -> Self {
self.merge_into_inner(Div::new().opacity(opacity));
self
}
pub fn flex_shrink(self) -> Self {
self.merge_into_inner(Div::new().flex_shrink());
self
}
pub fn flex_shrink_0(self) -> Self {
self.merge_into_inner(Div::new().flex_shrink_0());
self
}
pub fn transform(self, transform: blinc_core::Transform) -> Self {
self.merge_into_inner(Div::new().transform(transform));
self
}
pub fn overflow_clip(self) -> Self {
self.merge_into_inner(Div::new().overflow_clip());
self
}
pub fn overflow_y_scroll(self) -> Self {
self.merge_into_inner(Div::new().overflow_y_scroll());
self
}
pub fn cursor(self, cursor: crate::element::CursorStyle) -> Self {
self.merge_into_inner(Div::new().cursor(cursor));
self
}
pub fn cursor_pointer(self) -> Self {
self.cursor(crate::element::CursorStyle::Pointer)
}
pub fn cursor_text(self) -> Self {
self.cursor(crate::element::CursorStyle::Text)
}
pub fn absolute(self) -> Self {
self.merge_into_inner(Div::new().absolute());
self
}
pub fn relative(self) -> Self {
self.merge_into_inner(Div::new().relative());
self
}
pub fn top(self, px: f32) -> Self {
self.merge_into_inner(Div::new().top(px));
self
}
pub fn bottom(self, px: f32) -> Self {
self.merge_into_inner(Div::new().bottom(px));
self
}
pub fn left(self, px: f32) -> Self {
self.merge_into_inner(Div::new().left(px));
self
}
pub fn right(self, px: f32) -> Self {
self.merge_into_inner(Div::new().right(px));
self
}
pub fn child(self, child: impl ElementBuilder + 'static) -> Self {
self.merge_into_inner(Div::new().child(child));
self
}
pub fn children<I>(self, children: I) -> Self
where
I: IntoIterator,
I::Item: ElementBuilder + 'static,
{
self.merge_into_inner(Div::new().children(children));
self
}
pub fn on_click<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
tracing::debug!("Stateful::on_click - registering click handler");
self.event_handlers_cache.borrow_mut().on_click(handler);
tracing::debug!(
"Stateful::on_click - cache empty after: {}",
self.event_handlers_cache.borrow().is_empty()
);
self
}
pub fn on_mouse_down<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on_mouse_down(handler);
self
}
pub fn on_mouse_up<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_mouse_up(handler);
self
}
pub fn on_hover_enter<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on_hover_enter(handler);
self
}
pub fn on_hover_leave<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on_hover_leave(handler);
self
}
pub fn on_focus<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_focus(handler);
self
}
pub fn on_blur<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_blur(handler);
self
}
pub fn on_mount<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_mount(handler);
self
}
pub fn on_unmount<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_unmount(handler);
self
}
pub fn on_key_down<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_key_down(handler);
self
}
pub fn on_key_up<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_key_up(handler);
self
}
pub fn on_text_input<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on_text_input(handler);
self
}
pub fn on_scroll<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_scroll(handler);
self
}
pub fn on_mouse_move<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on(blinc_core::events::event_types::POINTER_MOVE, handler);
self
}
pub fn on_drag<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on(blinc_core::events::event_types::DRAG, handler);
self
}
pub fn on_drag_end<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on(blinc_core::events::event_types::DRAG_END, handler);
self
}
pub fn on_resize<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache.borrow_mut().on_resize(handler);
self
}
pub fn on_event<F>(self, event_type: blinc_core::events::EventType, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.event_handlers_cache
.borrow_mut()
.on(event_type, handler);
self
}
pub fn on_layout<F>(mut self, callback: F) -> Self
where
F: Fn(crate::element::ElementBounds) + Send + Sync + 'static,
{
self.layout_bounds_cb = Some(std::sync::Arc::new(callback));
self
}
pub fn bounds_storage(&self) -> crate::renderer::LayoutBoundsStorage {
Arc::clone(&self.layout_bounds)
}
pub fn bind(self, element_ref: &ElementRef<Self>) -> BoundStateful<S> {
element_ref.set(self);
BoundStateful {
storage: element_ref.storage(),
}
}
}
pub struct BoundStateful<S: StateTransitions> {
storage: Arc<Mutex<Option<Stateful<S>>>>,
}
impl<S: StateTransitions> BoundStateful<S> {
fn transform_inner<F>(self, f: F) -> Self
where
F: FnOnce(Stateful<S>) -> Stateful<S>,
{
let mut guard = self.storage.lock().unwrap();
if let Some(elem) = guard.take() {
*guard = Some(f(elem));
}
drop(guard);
self
}
pub fn on_state<F>(self, callback: F) -> Self
where
F: Fn(&S, &mut Div) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_state(callback))
}
pub fn w(self, px: f32) -> Self {
self.transform_inner(|s| s.w(px))
}
pub fn h(self, px: f32) -> Self {
self.transform_inner(|s| s.h(px))
}
pub fn w_full(self) -> Self {
self.transform_inner(|s| s.w_full())
}
pub fn h_full(self) -> Self {
self.transform_inner(|s| s.h_full())
}
pub fn size(self, w: f32, h: f32) -> Self {
self.transform_inner(|s| s.size(w, h))
}
pub fn square(self, size: f32) -> Self {
self.transform_inner(|s| s.square(size))
}
pub fn flex_row(self) -> Self {
self.transform_inner(|s| s.flex_row())
}
pub fn flex_col(self) -> Self {
self.transform_inner(|s| s.flex_col())
}
pub fn flex_grow(self) -> Self {
self.transform_inner(|s| s.flex_grow())
}
pub fn w_fit(self) -> Self {
self.transform_inner(|s| s.w_fit())
}
pub fn h_fit(self) -> Self {
self.transform_inner(|s| s.h_fit())
}
pub fn p(self, units: f32) -> Self {
self.transform_inner(|s| s.p(units))
}
pub fn px(self, units: f32) -> Self {
self.transform_inner(|s| s.px(units))
}
pub fn py(self, units: f32) -> Self {
self.transform_inner(|s| s.py(units))
}
pub fn gap(self, units: f32) -> Self {
self.transform_inner(|s| s.gap(units))
}
pub fn items_center(self) -> Self {
self.transform_inner(|s| s.items_center())
}
pub fn justify_center(self) -> Self {
self.transform_inner(|s| s.justify_center())
}
pub fn justify_between(self) -> Self {
self.transform_inner(|s| s.justify_between())
}
pub fn bg(self, color: impl Into<blinc_core::Brush>) -> Self {
let brush = color.into();
self.transform_inner(|s| s.bg(brush))
}
pub fn rounded(self, radius: f32) -> Self {
self.transform_inner(|s| s.rounded(radius))
}
pub fn border(self, width: f32, color: blinc_core::Color) -> Self {
self.transform_inner(|s| s.border(width, color))
}
pub fn border_color(self, color: blinc_core::Color) -> Self {
self.transform_inner(|s| s.border_color(color))
}
pub fn border_width(self, width: f32) -> Self {
self.transform_inner(|s| s.border_width(width))
}
pub fn shadow(self, shadow: blinc_core::Shadow) -> Self {
self.transform_inner(|s| s.shadow(shadow))
}
pub fn shadow_sm(self) -> Self {
self.transform_inner(|s| s.shadow_sm())
}
pub fn shadow_md(self) -> Self {
self.transform_inner(|s| s.shadow_md())
}
pub fn shadow_lg(self) -> Self {
self.transform_inner(|s| s.shadow_lg())
}
pub fn shadow_xl(self) -> Self {
self.transform_inner(|s| s.shadow_xl())
}
pub fn opacity(self, opacity: f32) -> Self {
self.transform_inner(|s| s.opacity(opacity))
}
pub fn flex_shrink(self) -> Self {
self.transform_inner(|s| s.flex_shrink())
}
pub fn flex_shrink_0(self) -> Self {
self.transform_inner(|s| s.flex_shrink_0())
}
pub fn transform_style(self, xform: blinc_core::Transform) -> Self {
self.transform_inner(|s| s.transform(xform))
}
pub fn overflow_clip(self) -> Self {
self.transform_inner(|s| s.overflow_clip())
}
pub fn overflow_y_scroll(self) -> Self {
self.transform_inner(|s| s.overflow_y_scroll())
}
pub fn cursor(self, cursor: crate::element::CursorStyle) -> Self {
self.transform_inner(|s| s.cursor(cursor))
}
pub fn cursor_pointer(self) -> Self {
self.cursor(crate::element::CursorStyle::Pointer)
}
pub fn cursor_text(self) -> Self {
self.cursor(crate::element::CursorStyle::Text)
}
pub fn child(self, child: impl ElementBuilder + 'static) -> Self {
self.transform_inner(|s| s.child(child))
}
pub fn on_click<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_click(handler))
}
pub fn on_mouse_down<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_mouse_down(handler))
}
pub fn on_mouse_up<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_mouse_up(handler))
}
pub fn on_hover_enter<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_hover_enter(handler))
}
pub fn on_hover_leave<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_hover_leave(handler))
}
pub fn on_focus<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_focus(handler))
}
pub fn on_blur<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_blur(handler))
}
pub fn on_mount<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_mount(handler))
}
pub fn on_unmount<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_unmount(handler))
}
pub fn on_key_down<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_key_down(handler))
}
pub fn on_key_up<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_key_up(handler))
}
pub fn on_scroll<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_scroll(handler))
}
pub fn on_mouse_move<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_mouse_move(handler))
}
pub fn on_resize<F>(self, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_resize(handler))
}
pub fn on_event<F>(self, event_type: blinc_core::events::EventType, handler: F) -> Self
where
F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_event(event_type, handler))
}
pub fn on_layout<F>(self, callback: F) -> Self
where
F: Fn(crate::element::ElementBounds) + Send + Sync + 'static,
{
self.transform_inner(|s| s.on_layout(callback))
}
}
impl<S: StateTransitions + Default> ElementBuilder for BoundStateful<S> {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.storage
.lock()
.unwrap()
.as_ref()
.map(|s| s.build(tree))
.expect("BoundStateful: element not bound")
}
fn render_props(&self) -> RenderProps {
self.storage
.lock()
.unwrap()
.as_ref()
.map(|s| s.render_props())
.unwrap_or_default()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
&[]
}
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Div
}
fn layout_animation_config(&self) -> Option<crate::layout_animation::LayoutAnimationConfig> {
self.storage
.lock()
.unwrap()
.as_ref()
.and_then(|s| s.layout_animation_config())
}
fn visual_animation_config(&self) -> Option<crate::visual_animation::VisualAnimationConfig> {
self.storage
.lock()
.unwrap()
.as_ref()
.and_then(|s| s.visual_animation_config())
}
}
impl<S: StateTransitions> ElementBuilder for Stateful<S> {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
{
let mut guard = self.shared_state.lock().unwrap();
guard.ancestor_motion_key = crate::motion::current_motion_key();
}
self.ensure_callback_invoked();
{
let mut inner = self.inner.borrow_mut();
let children = std::mem::take(&mut inner.children);
*self.children_cache.borrow_mut() = children;
}
{
let _cache = self.children_cache.borrow();
let _inner = self.inner.borrow_mut();
}
let inner = self.inner.borrow_mut();
let node = tree.create_node(inner.style.clone());
for child in self.children_cache.borrow().iter() {
let child_node = child.build(tree);
tree.add_child(node, child_node);
}
self.shared_state.lock().unwrap().node_id = Some(node);
let shared_for_css = Arc::clone(&self.shared_state);
register_stateful_base_updater(
node,
Arc::new(move |props: RenderProps| {
if let Ok(mut guard) = shared_for_css.lock() {
guard.base_render_props = Some(props);
}
}),
);
node
}
fn render_props(&self) -> RenderProps {
self.ensure_callback_invoked();
self.inner.borrow().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.ensure_callback_invoked();
unsafe {
let cache = self.children_cache.as_ptr();
if !(*cache).is_empty() {
return (*cache).as_slice();
}
let inner = self.inner.as_ptr();
(*inner).children.as_slice()
}
}
fn element_type_id(&self) -> ElementTypeId {
ElementTypeId::Div
}
fn element_id(&self) -> Option<&str> {
unsafe { (*self.inner.as_ptr()).element_id.as_deref() }
}
fn element_classes(&self) -> &[String] {
unsafe { &(*self.inner.as_ptr()).classes }
}
fn event_handlers(&self) -> Option<&crate::event_handler::EventHandlers> {
unsafe {
let cache = self.event_handlers_cache.as_ptr();
let handlers = &*cache;
if handlers.is_empty() {
None
} else {
Some(handlers)
}
}
}
fn layout_style(&self) -> Option<&taffy::Style> {
unsafe {
let inner = self.inner.as_ptr();
Some(&(*inner).style)
}
}
fn layout_bounds_storage(&self) -> Option<crate::renderer::LayoutBoundsStorage> {
Some(Arc::clone(&self.layout_bounds))
}
fn layout_bounds_callback(&self) -> Option<crate::renderer::LayoutBoundsCallback> {
self.layout_bounds_cb.clone()
}
fn layout_animation_config(&self) -> Option<crate::layout_animation::LayoutAnimationConfig> {
self.ensure_callback_invoked();
self.inner.borrow().layout_animation_config()
}
fn visual_animation_config(&self) -> Option<crate::visual_animation::VisualAnimationConfig> {
self.ensure_callback_invoked();
self.inner.borrow().visual_animation_config()
}
fn scroll_physics(&self) -> Option<crate::scroll::SharedScrollPhysics> {
self.ensure_callback_invoked();
self.inner.borrow().scroll_physics.clone()
}
}
pub type Button = Stateful<ButtonState>;
pub type Toggle = Stateful<ToggleState>;
pub type Checkbox = Stateful<CheckboxState>;
pub type TextField = Stateful<TextFieldState>;
pub type ScrollContainer = Stateful<ScrollState>;
#[deprecated(
since = "0.5.0",
note = "Use stateful::<S>().on_state(|ctx| ...) instead"
)]
pub fn stateful_from_handle<S: StateTransitions>(handle: SharedState<S>) -> Stateful<S> {
Stateful::with_shared_state(handle)
}
pub fn stateful_button() -> Button {
Stateful::new(ButtonState::Idle)
}
pub fn toggle(initially_on: bool) -> Toggle {
Stateful::new(if initially_on {
ToggleState::On
} else {
ToggleState::Off
})
}
pub fn stateful_checkbox(initially_checked: bool) -> Checkbox {
Stateful::new(if initially_checked {
CheckboxState::CheckedIdle
} else {
CheckboxState::UncheckedIdle
})
}
pub fn text_field() -> TextField {
Stateful::new(TextFieldState::Idle)
}
#[cfg(test)]
mod tests {
use super::*;
use blinc_core::events::event_types;
use blinc_core::{Brush, Color, CornerRadius};
use std::sync::atomic::{AtomicU32, Ordering};
#[test]
fn test_stateful_basic() {
let elem: Stateful<ButtonState> = Stateful::new(ButtonState::Idle)
.w(100.0)
.h(40.0)
.bg(Color::BLUE)
.rounded(8.0);
let mut tree = LayoutTree::new();
let _node = elem.build(&mut tree);
}
#[test]
fn test_state_callback_with_pattern_matching() {
let elem = stateful_button()
.w(100.0)
.h(40.0)
.on_state(|state, div| match state {
ButtonState::Idle => {
*div = div.swap().bg(Color::BLUE).rounded(4.0);
}
ButtonState::Hovered => {
*div = div.swap().bg(Color::GREEN).rounded(8.0);
}
ButtonState::Pressed => {
*div = div.swap().bg(Color::RED);
}
ButtonState::Disabled => {
*div = div.swap().bg(Color::GRAY);
}
});
let props = elem.render_props();
assert!(matches!(props.background, Some(Brush::Solid(c)) if c == Color::BLUE));
assert_eq!(props.border_radius, CornerRadius::uniform(4.0));
}
#[test]
#[ignore = "Test needs to be updated for new API"]
fn test_state_transition_with_enum() {
let elem = stateful_button()
.w(100.0)
.h(40.0)
.on_state(|state, container| match state {
ButtonState::Idle => {
container.merge(crate::div().bg(Color::BLUE));
}
ButtonState::Hovered => {
container.merge(crate::div().bg(Color::GREEN));
}
_ => {}
});
let props = elem.render_props();
assert!(matches!(props.background, Some(Brush::Solid(c)) if c == Color::BLUE));
let changed = elem.dispatch_state(ButtonState::Hovered);
assert!(changed);
assert_eq!(elem.state(), ButtonState::Hovered);
let props = elem.render_props();
assert!(matches!(props.background, Some(Brush::Solid(c)) if c == Color::GREEN));
let changed = elem.dispatch_state(ButtonState::Hovered);
assert!(!changed);
}
#[test]
fn test_handle_event() {
let elem = stateful_button()
.w(100.0)
.on_state(|state, div| match state {
ButtonState::Idle => {
*div = div.swap().bg(Color::BLUE);
}
ButtonState::Hovered => {
*div = div.swap().bg(Color::GREEN);
}
ButtonState::Pressed => {
*div = div.swap().bg(Color::RED);
}
_ => {}
});
assert_eq!(elem.state(), ButtonState::Idle);
let changed = elem.handle_event(event_types::POINTER_ENTER);
assert!(changed);
assert_eq!(elem.state(), ButtonState::Hovered);
let changed = elem.handle_event(event_types::POINTER_DOWN);
assert!(changed);
assert_eq!(elem.state(), ButtonState::Pressed);
let changed = elem.handle_event(event_types::POINTER_UP);
assert!(changed);
assert_eq!(elem.state(), ButtonState::Hovered);
}
#[test]
fn test_callback_is_called() {
let call_count = Arc::new(AtomicU32::new(0));
let call_count_clone = Arc::clone(&call_count);
let _elem = stateful_button().w(100.0).on_state(move |_state, _div| {
call_count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(call_count.load(Ordering::SeqCst), 1);
}
#[test]
#[ignore = "Test needs to be updated to match latest API"]
fn test_toggle_states() {
let t = toggle(false)
.w(50.0)
.h(30.0)
.on_state(|state, div| match state {
ToggleState::Off => {
*div = div.swap().bg(Color::GRAY);
}
ToggleState::On => {
*div = div.swap().bg(Color::GREEN);
}
});
assert_eq!(t.state(), ToggleState::Off);
let props = t.render_props();
assert!(matches!(props.background, Some(Brush::Solid(c)) if c == Color::GRAY));
t.handle_event(event_types::POINTER_UP);
assert_eq!(t.state(), ToggleState::On);
let props = t.render_props();
assert!(matches!(props.background, Some(Brush::Solid(c)) if c == Color::GREEN));
t.handle_event(event_types::POINTER_UP);
assert_eq!(t.state(), ToggleState::Off);
}
#[test]
fn test_checkbox_states() {
let cb = stateful_checkbox(false)
.square(24.0)
.on_state(|state, div| match state {
CheckboxState::UncheckedIdle => {
*div = div.swap().bg(Color::WHITE).rounded(4.0);
}
CheckboxState::UncheckedHovered => {
*div = div.swap().bg(Color::GRAY).rounded(4.0);
}
CheckboxState::CheckedIdle => {
*div = div.swap().bg(Color::BLUE).rounded(4.0);
}
CheckboxState::CheckedHovered => {
*div = div.swap().bg(Color::CYAN).rounded(4.0);
}
});
assert!(!cb.state().is_checked());
cb.handle_event(event_types::POINTER_ENTER);
assert_eq!(cb.state(), CheckboxState::UncheckedHovered);
assert!(cb.state().is_hovered());
cb.handle_event(event_types::POINTER_UP);
assert_eq!(cb.state(), CheckboxState::CheckedHovered);
assert!(cb.state().is_checked());
cb.handle_event(event_types::POINTER_LEAVE);
assert_eq!(cb.state(), CheckboxState::CheckedIdle);
assert!(cb.state().is_checked());
assert!(!cb.state().is_hovered());
}
#[test]
fn test_text_field_states() {
let field = text_field()
.w(200.0)
.h(40.0)
.on_state(|state, div| match state {
TextFieldState::Idle => {
*div = div.swap().bg(Color::WHITE).rounded(4.0);
}
TextFieldState::Hovered => {
*div = div.swap().bg(Color::WHITE).rounded(4.0);
}
TextFieldState::Focused => {
*div = div.swap().bg(Color::WHITE).rounded(4.0);
}
TextFieldState::FocusedHovered => {
*div = div.swap().bg(Color::WHITE).rounded(4.0);
}
TextFieldState::Disabled => {
*div = div.swap().bg(Color::GRAY);
}
});
assert_eq!(field.state(), TextFieldState::Idle);
assert!(!field.state().is_focused());
field.handle_event(event_types::POINTER_ENTER);
field.handle_event(event_types::POINTER_DOWN);
assert!(field.state().is_focused());
field.handle_event(event_types::BLUR);
assert!(!field.state().is_focused());
}
#[test]
fn test_disabled_button_ignores_events() {
let btn = Stateful::new(ButtonState::Disabled)
.w(100.0)
.on_state(|_state, _div| {});
assert_eq!(btn.state(), ButtonState::Disabled);
assert!(!btn.handle_event(event_types::POINTER_ENTER));
assert!(!btn.handle_event(event_types::POINTER_DOWN));
assert!(!btn.handle_event(event_types::POINTER_UP));
assert_eq!(btn.state(), ButtonState::Disabled);
}
#[test]
fn test_unit_state_ignores_all_events() {
let elem: Stateful<()> =
Stateful::new(())
.w(100.0)
.h(40.0)
.bg(Color::BLUE)
.on_state(|_state, div| {
div.set_bg(Color::RED);
});
assert_eq!(elem.state(), ());
assert!(!elem.handle_event(event_types::POINTER_ENTER));
assert!(!elem.handle_event(event_types::POINTER_DOWN));
assert!(!elem.handle_event(event_types::POINTER_UP));
assert!(!elem.handle_event(event_types::POINTER_LEAVE));
assert_eq!(elem.state(), ());
}
}