Port example 04
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
//! This crate is intentionally low-level. It is the substrate that a future proc-macro-driven
|
||||
//! component system can expand to, not the final ergonomic authoring API.
|
||||
|
||||
use std::any::Any;
|
||||
use std::any::{Any, TypeId, type_name};
|
||||
use std::cell::{Cell as StdCell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
@@ -25,7 +25,7 @@ use ruin_ui::{
|
||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||
|
||||
pub use ResourceState::{Pending, Ready};
|
||||
pub use ruin_app_proc_macros::{component, view};
|
||||
pub use ruin_app_proc_macros::{component, context_provider, view};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -94,6 +94,11 @@ impl Default for App {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn __render_mountable_for_test<M: Mountable>(mountable: &M) -> View {
|
||||
render_with_context(Rc::new(RenderState::default()), || mountable.render()).view
|
||||
}
|
||||
|
||||
pub trait Mountable: 'static {
|
||||
fn render(&self) -> View;
|
||||
}
|
||||
@@ -105,6 +110,10 @@ pub trait Component: 'static {
|
||||
fn render(&self) -> View;
|
||||
}
|
||||
|
||||
pub trait ContextKey: 'static {
|
||||
type Value: Clone + 'static;
|
||||
}
|
||||
|
||||
impl<T: Component> Mountable for T {
|
||||
fn render(&self) -> View {
|
||||
Component::render(self)
|
||||
@@ -1147,10 +1156,57 @@ impl<T: Clone> Memo<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ContextEntry {
|
||||
key: TypeId,
|
||||
value: Rc<dyn Any>,
|
||||
}
|
||||
|
||||
impl ContextEntry {
|
||||
fn new<C: ContextKey>(value: C::Value) -> Self {
|
||||
Self {
|
||||
key: TypeId::of::<C>(),
|
||||
value: Rc::new(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() -> 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> {
|
||||
let compute: Rc<RefCell<Box<dyn Fn() -> T>>> =
|
||||
Rc::new(RefCell::new(Box::new(compute) as Box<dyn Fn() -> T>));
|
||||
@@ -1434,6 +1490,19 @@ struct RenderContext {
|
||||
hook_index: Rc<StdCell<usize>>,
|
||||
element_index: Rc<StdCell<usize>>,
|
||||
side_effects: Rc<RefCell<RenderSideEffects>>,
|
||||
context_entries: Rc<Vec<ContextEntry>>,
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
fn with_context_entries(&self, context_entries: Rc<Vec<ContextEntry>>) -> Self {
|
||||
Self {
|
||||
state: Rc::clone(&self.state),
|
||||
hook_index: Rc::clone(&self.hook_index),
|
||||
element_index: Rc::clone(&self.element_index),
|
||||
side_effects: Rc::clone(&self.side_effects),
|
||||
context_entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderOutput {
|
||||
@@ -1451,8 +1520,15 @@ fn render_with_context(state: Rc<RenderState>, render: impl FnOnce() -> View) ->
|
||||
hook_index: Rc::new(StdCell::new(0)),
|
||||
element_index: Rc::new(StdCell::new(0)),
|
||||
side_effects: Rc::new(RefCell::new(RenderSideEffects::default())),
|
||||
context_entries: Rc::new(Vec::new()),
|
||||
};
|
||||
|
||||
let view = with_render_context(context.clone(), render);
|
||||
let side_effects = context.side_effects.borrow().clone();
|
||||
RenderOutput { view, side_effects }
|
||||
}
|
||||
|
||||
fn with_render_context(context: RenderContext, render: impl FnOnce() -> View) -> View {
|
||||
CURRENT_RENDER_CONTEXT.with(|slot| {
|
||||
let previous = slot.replace(Some(context.clone()));
|
||||
|
||||
@@ -1468,19 +1544,17 @@ fn render_with_context(state: Rc<RenderState>, render: impl FnOnce() -> View) ->
|
||||
}
|
||||
|
||||
let _guard = Guard { slot, previous };
|
||||
let view = render();
|
||||
let side_effects = context.side_effects.borrow().clone();
|
||||
RenderOutput { view, side_effects }
|
||||
render()
|
||||
})
|
||||
}
|
||||
|
||||
fn with_render_context_state<R>(f: impl FnOnce(&RenderContext) -> R) -> R {
|
||||
CURRENT_RENDER_CONTEXT.with(|slot| {
|
||||
let context = slot.borrow();
|
||||
let context = context
|
||||
.as_ref()
|
||||
let context = slot
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("ruin_app hooks can only run while rendering a mounted component");
|
||||
f(context)
|
||||
f(&context)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1837,13 +1911,13 @@ fn build_focused_ancestor_chain(
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
App, BlockWidget, ButtonBuilder, Children, Component, ContainerBuilder, FocusScope,
|
||||
FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable, Pending, Ready,
|
||||
Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget, Shortcut,
|
||||
App, BlockWidget, ButtonBuilder, Children, Component, ContainerBuilder, ContextKey,
|
||||
FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable, Pending,
|
||||
Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget, Shortcut,
|
||||
ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, View, WidgetRef, Window, block,
|
||||
button, colors, column, component, row, scroll_box, surfaces, text, use_effect, use_memo,
|
||||
use_resource, use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref,
|
||||
use_window_title, view,
|
||||
button, colors, column, component, context_provider, provide, row, scroll_box, surfaces,
|
||||
text, use_context, use_effect, use_memo, use_resource, use_shortcut,
|
||||
use_shortcut_with_context, use_signal, use_widget_ref, use_window_title, view,
|
||||
};
|
||||
pub use ruin_ui::{
|
||||
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton,
|
||||
@@ -1851,6 +1925,68 @@ pub mod prelude {
|
||||
TextFontFamily, TextStyle, TextWrap, UiSize,
|
||||
};
|
||||
}
|
||||
|
||||
struct SignalInner<T> {
|
||||
cell: ruin_reactivity::Cell<T>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct NamedValue(&'static str);
|
||||
|
||||
struct OuterContext;
|
||||
struct InnerContext;
|
||||
|
||||
impl ContextKey for OuterContext {
|
||||
type Value = NamedValue;
|
||||
}
|
||||
|
||||
impl ContextKey for InnerContext {
|
||||
type Value = NamedValue;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_context_distinguishes_marker_types_for_same_value_type() {
|
||||
let seen_outer = Rc::new(RefCell::new(None::<NamedValue>));
|
||||
let seen_inner = Rc::new(RefCell::new(None::<NamedValue>));
|
||||
|
||||
let _ = render_with_context(Rc::new(RenderState::default()), {
|
||||
let seen_outer = Rc::clone(&seen_outer);
|
||||
let seen_inner = Rc::clone(&seen_inner);
|
||||
move || {
|
||||
provide::<OuterContext>(NamedValue("outer"), || {
|
||||
provide::<InnerContext>(NamedValue("inner"), || {
|
||||
*seen_outer.borrow_mut() = Some(use_context::<OuterContext>());
|
||||
*seen_inner.borrow_mut() = Some(use_context::<InnerContext>());
|
||||
View::from_element(Element::column())
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(*seen_outer.borrow(), Some(NamedValue("outer")));
|
||||
assert_eq!(*seen_inner.borrow(), Some(NamedValue("inner")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nearer_provider_shadows_outer_provider_of_same_marker() {
|
||||
let seen_value = Rc::new(RefCell::new(None::<NamedValue>));
|
||||
|
||||
let _ = render_with_context(Rc::new(RenderState::default()), {
|
||||
let seen_value = Rc::clone(&seen_value);
|
||||
move || {
|
||||
provide::<OuterContext>(NamedValue("outer"), || {
|
||||
provide::<OuterContext>(NamedValue("inner"), || {
|
||||
*seen_value.borrow_mut() = Some(use_context::<OuterContext>());
|
||||
View::from_element(Element::column())
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(*seen_value.borrow(), Some(NamedValue("inner")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user