Merge origin/main into macos

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Will Temple
2026-05-16 16:38:10 -04:00
66 changed files with 9558 additions and 2326 deletions

419
lib/ruin_app/src/hooks.rs Normal file
View File

@@ -0,0 +1,419 @@
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(),
)
}