420 lines
11 KiB
Rust
420 lines
11 KiB
Rust
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<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,
|
|
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<Option<ElementId>>,
|
|
}
|
|
|
|
#[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<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_interaction_state<T: 'static>(widget_ref: WidgetRef<T>) -> 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<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(),
|
|
)
|
|
}
|