use std::any::TypeId; use std::any::type_name; use std::cell::Cell as StdCell; use std::future::Future; use std::marker::PhantomData; use std::rc::Rc; use ruin_ui::{ElementId, InteractionTree, KeyboardEvent, KeyboardEventKind, KeyboardKey}; use crate::ContextKey; use crate::context::{ ContextEntry, MemoSlot, ResourceSlot, with_hook_slot, with_render_context, with_render_context_state, }; use crate::input::ShortcutBinding; pub struct SignalInner { pub(crate) cell: ruin_reactivity::Cell, } pub struct Signal { pub(crate) inner: Rc>, } impl Clone for Signal { fn clone(&self) -> Self { Self { inner: Rc::clone(&self.inner), } } } impl Signal { pub(crate) fn new(initial: T) -> Self { Self { inner: Rc::new(SignalInner { cell: ruin_reactivity::cell(initial), }), } } pub fn with(&self, f: impl FnOnce(&T) -> R) -> R { self.inner.cell.with(f) } pub fn replace(&self, value: T) -> T { self.inner.cell.replace(value) } pub fn update(&self, f: impl FnOnce(&mut T) -> R) -> R { self.inner.cell.update(f) } } impl Signal { pub fn get(&self) -> T { self.inner.cell.get() } } impl Signal { pub fn set(&self, value: T) -> Option { self.inner.cell.set(value) } } pub struct Memo { pub(crate) compute: Rc T>, } impl Clone for Memo { fn clone(&self) -> Self { Self { compute: Rc::clone(&self.compute), } } } impl Memo { pub fn with(&self, f: impl FnOnce(&T) -> R) -> R { let value = (self.compute)(); f(&value) } } impl Memo { pub fn get(&self) -> T { (self.compute)() } } pub fn use_signal(initial: impl FnOnce() -> T) -> Signal { with_hook_slot(|| Signal::new(initial()), |signal| signal.clone()) } pub fn use_context() -> C::Value { with_render_context_state(|context| { context .context_entries .iter() .rev() .find_map(|entry| { (entry.key == TypeId::of::()) .then(|| entry.value.downcast_ref::()) .flatten() .cloned() }) .unwrap_or_else(|| { panic!( "missing context provider for {} while rendering", type_name::() ) }) }) } pub fn provide( value: C::Value, render: impl FnOnce() -> crate::view::View, ) -> crate::view::View { with_render_context_state(|context| { let mut context_entries = (*context.context_entries).clone(); context_entries.push(ContextEntry::new::(value)); with_render_context( context.with_context_entries(Rc::new(context_entries)), render, ) }) } pub fn use_memo(compute: impl Fn() -> T + 'static) -> Memo { use std::cell::RefCell; let compute: Rc T>>> = Rc::new(RefCell::new(Box::new(compute) as Box T>)); with_hook_slot( { let compute = Rc::clone(&compute); move || MemoSlot::new(compute) }, |slot: &mut MemoSlot| { slot.replace_compute(Rc::clone(&compute)); slot.handle.clone() }, ) } pub fn use_effect(effect: impl Fn() + 'static) { with_hook_slot(|| ruin_reactivity::effect(effect), |_| ()); } pub fn use_window_title(compute: impl FnOnce() -> String) { with_render_context_state(|context| { context.side_effects.borrow_mut().window_title = Some(compute()); }); } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Key { Character(char), Enter, Backspace, Escape, Delete, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, Home, End, } impl Key { pub(crate) fn matches(&self, event: &KeyboardEvent) -> bool { match (self, &event.key) { (Self::Character(expected), KeyboardKey::Character(actual)) => actual .chars() .next() .is_some_and(|actual| actual.eq_ignore_ascii_case(expected)), (Self::Enter, KeyboardKey::Enter) => true, (Self::Backspace, KeyboardKey::Backspace) => true, (Self::Escape, KeyboardKey::Escape) => true, (Self::Delete, KeyboardKey::Delete) => true, (Self::ArrowLeft, KeyboardKey::ArrowLeft) => true, (Self::ArrowRight, KeyboardKey::ArrowRight) => true, (Self::ArrowUp, KeyboardKey::ArrowUp) => true, (Self::ArrowDown, KeyboardKey::ArrowDown) => true, (Self::Home, KeyboardKey::Home) => true, (Self::End, KeyboardKey::End) => true, _ => false, } } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Shortcut { pub(crate) key: Key, pub(crate) control: bool, pub(crate) shift: bool, pub(crate) alt: bool, pub(crate) super_key: bool, } impl Shortcut { pub fn new(key: Key) -> Self { Self { key, control: false, shift: false, alt: false, super_key: false, } } pub fn with_ctrl(mut self) -> Self { self.control = true; self } pub fn with_shift(mut self) -> Self { self.shift = true; self } pub(crate) fn matches(&self, event: &KeyboardEvent) -> bool { if event.kind != KeyboardEventKind::Pressed { return false; } if !self.key.matches(event) { return false; } if event.modifiers.control != self.control { return false; } if event.modifiers.alt != self.alt { return false; } if event.modifiers.super_key != self.super_key { return false; } // For character shortcuts the produced character already encodes the shift // state, so don't additionally require that Shift is (un)held. if !matches!(self.key, Key::Character(_)) && event.modifiers.shift != self.shift { return false; } true } } #[derive(Clone)] pub struct FocusScope { pub(crate) element_id: Signal>, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct InteractionState { pub hovered: bool, pub pressed: bool, } #[derive(Clone)] pub enum ShortcutScope { Application, FocusedWithin(FocusScope), } impl ShortcutScope { pub(crate) fn matches( &self, focused_element: Option, interaction_tree: &InteractionTree, ) -> bool { match self { Self::Application => true, Self::FocusedWithin(scope) => { let Some(scope_element) = scope.element_id.get() else { return false; }; let Some(focused_element) = focused_element else { return false; }; crate::input::element_contains_element( interaction_tree, scope_element, focused_element, ) } } } } pub struct WidgetRef { pub(crate) element_id: Signal>, pub(crate) _marker: PhantomData T>, } impl Clone for WidgetRef { fn clone(&self) -> Self { Self { element_id: self.element_id.clone(), _marker: PhantomData, } } } impl WidgetRef { pub(crate) fn new() -> Self { Self { element_id: Signal::new(None), _marker: PhantomData, } } pub fn element_id(&self) -> Option { self.element_id.get() } pub fn focus_scope(&self) -> FocusScope { FocusScope { element_id: self.element_id.clone(), } } } pub struct ScrollBoxWidget; pub struct BlockWidget; pub fn use_widget_ref() -> WidgetRef { with_hook_slot(WidgetRef::new, |widget_ref: &mut WidgetRef| { widget_ref.clone() }) } pub fn use_interaction_state(widget_ref: WidgetRef) -> InteractionState { with_render_context_state(|context| { let Some(element_id) = widget_ref.element_id() else { return InteractionState::default(); }; InteractionState { hovered: context.interaction.hovered.contains(&element_id), pressed: context.interaction.pressed.contains(&element_id), } }) } pub fn use_shortcut(shortcut: Shortcut, scope: ShortcutScope, action: impl Fn() + 'static) { use_shortcut_with_context(shortcut, scope, move |_| action()); } pub fn use_shortcut_with_context( shortcut: Shortcut, scope: ShortcutScope, action: impl Fn(&InteractionTree) + 'static, ) { with_render_context_state(|context| { context .side_effects .borrow_mut() .shortcuts .push(ShortcutBinding { shortcut, scope, action: Rc::new(action), }); }); } #[derive(Clone)] pub struct Resource { pub(crate) state: Signal>, } impl Resource { pub fn read(&self) -> ResourceState { self.state.get() } } #[derive(Clone)] pub enum ResourceState { Pending, Ready(std::result::Result), } pub fn use_resource(factory: F) -> Resource where T: Clone + 'static, E: Clone + 'static, Fut: Future> + 'static, F: Fn() -> Fut + 'static, { with_hook_slot( || { let resource = Resource { state: Signal::new(ResourceState::Pending), }; let generation = Rc::new(StdCell::new(0_u64)); let _effect = ruin_reactivity::effect({ let resource = resource.clone(); let generation = Rc::clone(&generation); move || { let next_generation = generation.get().wrapping_add(1); generation.set(next_generation); let _ = resource.state.replace(ResourceState::Pending); let resource = resource.clone(); let future = factory(); let generation = Rc::clone(&generation); std::mem::drop(ruin_runtime::queue_future(async move { let result = future.await; if generation.get() == next_generation { let _ = resource.state.replace(ResourceState::Ready(result)); } })); } }); ResourceSlot { resource, _effect } }, |slot: &mut ResourceSlot| slot.resource.clone(), ) }