use std::sync::Arc;
use blinc_core::context_state::MotionAnimationState;
use blinc_core::BlincContextState;
use crate::element::{ElementBounds, RenderProps};
use crate::tree::LayoutNodeId;
use super::registry::{ElementRegistry, OnReadyCallback};
use super::ScrollOptions;
#[derive(Clone)]
pub struct ElementHandle<T = ()> {
string_id: String,
node_id: LayoutNodeId,
registry: Arc<ElementRegistry>,
_marker: std::marker::PhantomData<T>,
}
impl<T> ElementHandle<T> {
pub fn new(string_id: impl Into<String>, registry: Arc<ElementRegistry>) -> Self {
let string_id = string_id.into();
let node_id = registry.get(&string_id).unwrap_or_default();
Self {
string_id,
node_id,
registry,
_marker: std::marker::PhantomData,
}
}
pub fn node_id(&self) -> LayoutNodeId {
self.registry.get(&self.string_id).unwrap_or(self.node_id)
}
pub fn id(&self) -> &str {
&self.string_id
}
pub fn exists(&self) -> bool {
self.registry.get(&self.string_id).is_some()
}
pub fn bounds(&self) -> Option<ElementBounds> {
let bounds = self.registry.get_bounds(&self.string_id)?;
Some(ElementBounds::new(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
))
}
pub fn is_visible(&self) -> bool {
let Some(bounds) = self.registry.get_bounds(&self.string_id) else {
return false;
};
if let Some(ctx) = BlincContextState::try_get() {
let (vw, vh) = ctx.viewport_size();
bounds.x < vw
&& bounds.x + bounds.width > 0.0
&& bounds.y < vh
&& bounds.y + bounds.height > 0.0
} else {
true
}
}
pub fn parent(&self) -> Option<ElementHandle<()>> {
let current_node_id = self.node_id();
let parent_node_id = self.registry.get_parent(current_node_id)?;
let parent_string_id = self.registry.get_id(parent_node_id)?;
Some(ElementHandle::new(parent_string_id, self.registry.clone()))
}
pub fn ancestors(&self) -> impl Iterator<Item = ElementHandle<()>> {
let current_node_id = self.node_id();
let ancestors = self.registry.ancestors(current_node_id);
let registry = self.registry.clone();
ancestors.into_iter().filter_map(move |id| {
let string_id = registry.get_id(id)?;
Some(ElementHandle::new(string_id, registry.clone()))
})
}
pub fn scroll_into_view(&self) {
self.scroll_into_view_with(ScrollOptions::default());
}
pub fn scroll_into_view_with(&self, _options: ScrollOptions) {
if let Some(ctx) = BlincContextState::try_get() {
ctx.scroll_element_into_view(&self.string_id);
}
}
pub fn focus(&self) {
if let Some(ctx) = BlincContextState::try_get() {
ctx.set_focus(Some(&self.string_id));
}
}
pub fn blur(&self) {
if let Some(ctx) = BlincContextState::try_get() {
if ctx.is_focused(&self.string_id) {
ctx.set_focus(None);
}
}
}
pub fn is_focused(&self) -> bool {
BlincContextState::try_get()
.map(|ctx| ctx.is_focused(&self.string_id))
.unwrap_or(false)
}
pub fn emit_signal(&self, signal_id: blinc_core::SignalId) {
if let Some(ctx) = BlincContextState::try_get() {
ctx.notify_stateful_deps(&[signal_id]);
}
}
pub fn mark_dirty(&self) {
if let Some(ctx) = BlincContextState::try_get() {
ctx.request_rebuild();
}
}
pub fn mark_dirty_subtree(&self, new_children: crate::div::Div) {
if let Some(node_id) = self.registry.get(&self.string_id) {
crate::stateful::queue_subtree_rebuild(node_id, new_children);
}
}
pub fn mark_visual_dirty(&self, props: RenderProps) {
if let Some(node_id) = self.registry.get(&self.string_id) {
crate::stateful::queue_prop_update(node_id, props);
}
}
pub fn click(&self) {
self.dispatch_event(ElementEvent::Click { x: 0.0, y: 0.0 });
}
pub fn click_at(&self, x: f32, y: f32) {
self.dispatch_event(ElementEvent::Click { x, y });
}
pub fn hover(&self, enter: bool) {
if enter {
self.dispatch_event(ElementEvent::MouseEnter);
} else {
self.dispatch_event(ElementEvent::MouseLeave);
}
}
pub fn dispatch_event(&self, _event: ElementEvent) {
}
pub fn on_ready<F>(&self, callback: F)
where
F: Fn(ElementBounds) + Send + Sync + 'static,
{
self.registry
.register_on_ready_for_id(&self.string_id, Arc::new(callback));
}
pub fn on_ready_arc(&self, callback: OnReadyCallback) {
self.registry
.register_on_ready_for_id(&self.string_id, callback);
}
}
#[derive(Debug, Clone)]
pub enum ElementEvent {
Click { x: f32, y: f32 },
MouseEnter,
MouseLeave,
Focus,
Blur,
KeyDown {
key: u32, modifiers: u8, },
Custom(u32),
}
pub trait Queryable: Sized {
fn from_handle(handle: &ElementHandle<()>) -> Option<Self>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle_creation() {
let registry = Arc::new(ElementRegistry::new());
let node_id = LayoutNodeId::default();
registry.register("test", node_id);
let handle: ElementHandle<()> = ElementHandle::new("test", registry);
assert_eq!(handle.node_id(), node_id);
assert_eq!(handle.id(), "test");
assert!(handle.exists());
}
#[test]
fn test_handle_for_nonexistent_element() {
let registry = Arc::new(ElementRegistry::new());
let handle: ElementHandle<()> = ElementHandle::new("future-element", registry);
assert_eq!(handle.id(), "future-element");
assert!(!handle.exists());
}
#[test]
fn test_parent_traversal() {
let registry = Arc::new(ElementRegistry::new());
let parent_id = LayoutNodeId::default();
let child_id = LayoutNodeId::default();
registry.register("parent", parent_id);
registry.register("child", child_id);
registry.register_parent(child_id, parent_id);
let child_handle: ElementHandle<()> = ElementHandle::new("child", registry);
let parent = child_handle.parent();
assert!(parent.is_some());
}
#[test]
fn test_handle_on_ready_registers_callback() {
let registry = Arc::new(ElementRegistry::new());
let handle: ElementHandle<()> = ElementHandle::new("my-element", registry.clone());
handle.on_ready(|_bounds| {
});
assert!(registry.has_pending_on_ready());
let pending = registry.take_pending_on_ready();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].0, "my-element");
}
#[test]
fn test_handle_on_ready_uses_string_id() {
let registry = Arc::new(ElementRegistry::new());
let node_id = LayoutNodeId::default();
registry.register("progress-bar", node_id);
let handle: ElementHandle<()> = ElementHandle::new("progress-bar", registry.clone());
handle.on_ready(|_| {});
let pending = registry.take_pending_on_ready();
assert_eq!(pending[0].0, "progress-bar");
}
#[test]
fn test_handle_on_ready_skips_if_already_triggered() {
let registry = Arc::new(ElementRegistry::new());
registry.mark_on_ready_triggered("my-element");
let handle: ElementHandle<()> = ElementHandle::new("my-element", registry.clone());
handle.on_ready(|_| {});
assert!(!registry.has_pending_on_ready());
}
#[test]
fn test_handle_on_ready_works_before_element_exists() {
let registry = Arc::new(ElementRegistry::new());
let handle: ElementHandle<()> = ElementHandle::new("future-element", registry.clone());
assert!(!handle.exists());
handle.on_ready(|_| {});
assert!(registry.has_pending_on_ready());
let pending = registry.take_pending_on_ready();
assert_eq!(pending[0].0, "future-element");
}
#[test]
fn test_handle_on_ready_arc() {
let registry = Arc::new(ElementRegistry::new());
let handle: ElementHandle<()> = ElementHandle::new("my-element", registry.clone());
let callback: OnReadyCallback = Arc::new(|_| {});
handle.on_ready_arc(callback);
assert!(registry.has_pending_on_ready());
}
}
#[derive(Clone, Debug)]
pub struct MotionHandle {
key: String,
state: MotionAnimationState,
}
impl MotionHandle {
pub fn new(key: impl Into<String>) -> Self {
let key = key.into();
let state = BlincContextState::try_get()
.map(|ctx| ctx.query_motion(&key))
.unwrap_or(MotionAnimationState::NotFound);
Self { key, state }
}
pub fn key(&self) -> &str {
&self.key
}
pub fn state(&self) -> MotionAnimationState {
self.state
}
pub fn is_animating(&self) -> bool {
self.state.is_animating()
}
pub fn is_settled(&self) -> bool {
self.state.is_settled()
}
pub fn is_suspended(&self) -> bool {
self.state.is_suspended()
}
pub fn is_entering(&self) -> bool {
self.state.is_entering()
}
pub fn is_exiting(&self) -> bool {
self.state.is_exiting()
}
pub fn progress(&self) -> f32 {
self.state.progress()
}
pub fn exists(&self) -> bool {
!matches!(self.state, MotionAnimationState::NotFound)
}
pub fn start(&self) {
crate::queue_global_motion_start(self.key.clone());
}
pub fn cancel_exit(&self) {
crate::queue_global_motion_exit_cancel(self.key.clone());
}
pub fn exit(&self) {
crate::queue_global_motion_exit_start(self.key.clone());
}
}