Refactor ruin_app, add some README files, minor usability in reactivity
This commit is contained in:
374
lib/ruin_app/src/hooks.rs
Normal file
374
lib/ruin_app/src/hooks.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
use std::any::type_name;
|
||||
use std::any::TypeId;
|
||||
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<T> {
|
||||
pub(crate) cell: ruin_reactivity::Cell<T>,
|
||||
}
|
||||
|
||||
pub struct Signal<T> {
|
||||
pub(crate) inner: Rc<SignalInner<T>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Signal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Rc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Signal<T> {
|
||||
pub(crate) fn new(initial: T) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(SignalInner {
|
||||
cell: ruin_reactivity::cell(initial),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with<R>(&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<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
|
||||
self.inner.cell.update(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> Signal<T> {
|
||||
pub fn get(&self) -> T {
|
||||
self.inner.cell.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + 'static> Signal<T> {
|
||||
pub fn set(&self, value: T) -> Option<T> {
|
||||
self.inner.cell.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Memo<T> {
|
||||
pub(crate) compute: Rc<dyn Fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Memo<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
compute: Rc::clone(&self.compute),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Memo<T> {
|
||||
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
|
||||
let value = (self.compute)();
|
||||
f(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Memo<T> {
|
||||
pub fn get(&self) -> T {
|
||||
(self.compute)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_signal<T: 'static>(initial: impl FnOnce() -> T) -> Signal<T> {
|
||||
with_hook_slot(|| Signal::new(initial()), |signal| signal.clone())
|
||||
}
|
||||
|
||||
pub fn use_context<C: ContextKey>() -> C::Value {
|
||||
with_render_context_state(|context| {
|
||||
context
|
||||
.context_entries
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|entry| {
|
||||
(entry.key == TypeId::of::<C>())
|
||||
.then(|| entry.value.downcast_ref::<C::Value>())
|
||||
.flatten()
|
||||
.cloned()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"missing context provider for {} while rendering",
|
||||
type_name::<C>()
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn provide<C: ContextKey>(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::<C>(value));
|
||||
with_render_context(
|
||||
context.with_context_entries(Rc::new(context_entries)),
|
||||
render,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn use_memo<T: 'static>(compute: impl Fn() -> T + 'static) -> Memo<T> {
|
||||
use std::cell::RefCell;
|
||||
let compute: Rc<RefCell<Box<dyn Fn() -> T>>> =
|
||||
Rc::new(RefCell::new(Box::new(compute) as Box<dyn Fn() -> T>));
|
||||
with_hook_slot(
|
||||
{
|
||||
let compute = Rc::clone(&compute);
|
||||
move || MemoSlot::new(compute)
|
||||
},
|
||||
|slot: &mut MemoSlot<T>| {
|
||||
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,
|
||||
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::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 {
|
||||
event.kind == KeyboardEventKind::Pressed
|
||||
&& self.key.matches(event)
|
||||
&& event.modifiers.control == self.control
|
||||
&& event.modifiers.shift == self.shift
|
||||
&& event.modifiers.alt == self.alt
|
||||
&& event.modifiers.super_key == self.super_key
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FocusScope {
|
||||
pub(crate) element_id: Signal<Option<ElementId>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ShortcutScope {
|
||||
Application,
|
||||
FocusedWithin(FocusScope),
|
||||
}
|
||||
|
||||
impl ShortcutScope {
|
||||
pub(crate) fn matches(
|
||||
&self,
|
||||
focused_element: Option<ElementId>,
|
||||
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<T> {
|
||||
pub(crate) element_id: Signal<Option<ElementId>>,
|
||||
pub(crate) _marker: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for WidgetRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
element_id: self.element_id.clone(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> WidgetRef<T> {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
element_id: Signal::new(None),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element_id(&self) -> Option<ElementId> {
|
||||
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<T: 'static>() -> WidgetRef<T> {
|
||||
with_hook_slot(WidgetRef::new, |widget_ref: &mut WidgetRef<T>| {
|
||||
widget_ref.clone()
|
||||
})
|
||||
}
|
||||
|
||||
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<T, E> {
|
||||
pub(crate) state: Signal<ResourceState<T, E>>,
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static, E: Clone + 'static> Resource<T, E> {
|
||||
pub fn read(&self) -> ResourceState<T, E> {
|
||||
self.state.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ResourceState<T, E> {
|
||||
Pending,
|
||||
Ready(std::result::Result<T, E>),
|
||||
}
|
||||
|
||||
pub fn use_resource<T, E, Fut, F>(factory: F) -> Resource<T, E>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
E: Clone + 'static,
|
||||
Fut: Future<Output = std::result::Result<T, E>> + '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<T, E>| slot.resource.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user