use std::any::{Any, TypeId};
use std::cell::{Cell, OnceCell, RefCell};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::rc::Rc;
use frus_core::{Rect, Scene};
use frus_layout::Style;
use crate::callback::Callback;
use crate::interaction::{Status, WidgetId};
use crate::runtime::Runtime;
use crate::theme::Theme;
use crate::widget::Widget;
thread_local! {
static REBUILD: Cell<bool> = const { Cell::new(false) };
static DEPTH: Cell<u32> = const { Cell::new(0) };
static INTERVALS: RefCell<Vec<Interval>> = const { RefCell::new(Vec::new()) };
static BACK_BLOCKS: Cell<u32> = const { Cell::new(0) };
}
#[inline(never)]
fn depth() -> u32 {
DEPTH.with(|d| d.get())
}
#[inline(never)]
fn push_interval(interval: Interval) {
INTERVALS.with(|intervals| intervals.borrow_mut().push(interval));
}
#[inline(never)]
pub fn back_blocked() -> bool {
BACK_BLOCKS.with(|blocks| blocks.get() > 0)
}
#[derive(Clone)]
pub struct Interval {
pub id: u64,
pub period: std::time::Duration,
pub callback: Callback,
}
#[inline(never)]
pub fn intervals() -> Vec<Interval> {
INTERVALS.with(|intervals| intervals.borrow().clone())
}
#[inline(never)]
pub fn request_rebuild() {
REBUILD.with(|flag| flag.set(true));
}
#[inline(never)]
pub fn rebuild_requested() -> bool {
REBUILD.with(|flag| flag.get())
}
#[inline(never)]
pub fn take_rebuild_request() -> bool {
REBUILD.with(|flag| flag.replace(false))
}
struct Deeper(u32);
impl Deeper {
#[inline(never)]
fn enter() -> Self {
let now = DEPTH.with(|d| d.get());
DEPTH.with(|d| d.set(now + 1));
Deeper(now)
}
}
impl Drop for Deeper {
#[inline(never)]
fn drop(&mut self) {
DEPTH.with(|d| d.set(self.0));
}
}
type StateKey = (WidgetId, u32);
#[derive(Default)]
pub struct StateStore {
entries: RefCell<HashMap<StateKey, Entry>>,
epoch: Cell<u64>,
building: Cell<bool>,
effects: RefCell<Vec<PendingEffect>>,
services: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
restoring: RefCell<HashMap<(StateKey, String), String>>,
}
struct Entry {
kind: TypeId,
epoch: u64,
state: Option<Rc<dyn Any>>,
hooks: Rc<Hooks>,
dispose: Option<Box<dyn FnOnce()>>,
save: Option<Saver>,
}
struct Saver {
name: &'static str,
save: Box<dyn Fn() -> Option<String>>,
}
impl Entry {
fn dispose_all(self) {
if let Some(dispose) = self.dispose {
dispose();
}
self.hooks.dispose();
}
}
type Cleanup = Box<dyn FnOnce()>;
struct PendingEffect {
hooks: Rc<Hooks>,
index: usize,
run: Box<dyn FnOnce() -> Option<Cleanup>>,
}
impl StateStore {
#[inline(never)]
pub fn begin_build(&self) {
self.epoch.set(self.epoch.get() + 1);
self.building.set(true);
INTERVALS.with(|intervals| intervals.borrow_mut().clear());
BACK_BLOCKS.with(|blocks| blocks.set(0));
}
pub fn end_frame(&self) {
if self.building.replace(false) {
self.sweep();
}
self.flush_effects();
}
pub fn snapshot(&self) -> String {
let mut lines: Vec<String> = self
.entries
.borrow()
.iter()
.filter_map(|((id, depth), entry)| {
let saver = entry.save.as_ref()?;
let saved = (saver.save)()?;
Some(format!(
"{:x}\t{depth}\t{}\t{}",
id.as_u64(),
saver.name,
escape(&saved)
))
})
.collect();
lines.sort();
lines.join("\n")
}
pub fn restore_from(&self, snapshot: &str) {
let mut restoring = self.restoring.borrow_mut();
for line in snapshot.lines() {
let mut fields = line.splitn(4, '\t');
let (Some(id), Some(depth), Some(name), Some(saved)) =
(fields.next(), fields.next(), fields.next(), fields.next())
else {
continue;
};
let (Ok(id), Ok(depth)) = (u64::from_str_radix(id, 16), depth.parse::<u32>()) else {
continue;
};
restoring.insert(
((WidgetId::from_u64(id), depth), name.to_string()),
unescape(saved),
);
}
}
pub fn provide<T: 'static>(&self, value: Rc<T>) {
self.services.borrow_mut().insert(TypeId::of::<T>(), value);
}
pub fn service<T: 'static>(&self) -> Option<Rc<T>> {
self.services
.borrow()
.get(&TypeId::of::<T>())
.and_then(|s| s.clone().downcast::<T>().ok())
}
pub fn len(&self) -> usize {
self.entries.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.entries.borrow().is_empty()
}
fn sweep(&self) {
let epoch = self.epoch.get();
let stale: Vec<Entry> = {
let mut entries = self.entries.borrow_mut();
let keys: Vec<StateKey> = entries
.iter()
.filter(|(_, entry)| entry.epoch < epoch)
.map(|(key, _)| *key)
.collect();
keys.into_iter()
.filter_map(|key| entries.remove(&key))
.collect()
};
for entry in stale {
entry.dispose_all();
}
}
fn flush_effects(&self) {
let pending: Vec<PendingEffect> = std::mem::take(&mut *self.effects.borrow_mut());
for effect in pending {
if effect.hooks.disposed.get() {
continue;
}
let previous = effect.hooks.take_cleanup(effect.index);
if let Some(cleanup) = previous {
cleanup();
}
let cleanup = (effect.run)();
effect.hooks.set_cleanup(effect.index, cleanup);
}
}
fn hooks_for(&self, key: StateKey, kind: TypeId) -> Rc<Hooks> {
let stale = {
let mut entries = self.entries.borrow_mut();
match entries.get_mut(&key) {
Some(entry) if entry.kind == kind => {
entry.epoch = self.epoch.get();
return entry.hooks.clone();
}
Some(_) => entries.remove(&key),
None => None,
}
};
if let Some(stale) = stale {
stale.dispose_all();
}
let hooks = Rc::new(Hooks::default());
self.entries.borrow_mut().insert(
key,
Entry {
kind,
epoch: self.epoch.get(),
state: None,
hooks: hooks.clone(),
dispose: None,
save: None,
},
);
hooks
}
fn mount<W: StatefulWidget>(&self, key: StateKey, widget: Rc<W>) -> Mounted<W> {
let kind = TypeId::of::<W>();
let found = {
let mut entries = self.entries.borrow_mut();
entries
.get_mut(&key)
.filter(|entry| entry.kind == kind)
.map(|entry| {
entry.epoch = self.epoch.get();
(entry.state.clone(), entry.hooks.clone())
})
};
if let Some((Some(state), hooks)) = found {
let cell = state
.downcast::<StateCell<W::State>>()
.unwrap_or_else(|_| unreachable!("an entry's kind says what its state is"));
let previous = std::mem::replace(&mut *cell.widget.borrow_mut(), widget);
return Mounted {
cell,
hooks,
previous: Some(previous),
};
}
let stale = self.entries.borrow_mut().remove(&key);
if let Some(stale) = stale {
stale.dispose_all();
}
let cell = Rc::new(StateCell {
state: RefCell::new(widget.create_state()),
widget: RefCell::new(widget),
mounted: Cell::new(true),
});
let name = std::any::type_name::<W::State>();
let saved = self.restoring.borrow_mut().remove(&(key, name.to_string()));
if let Some(saved) = saved {
cell.state.borrow_mut().restore(&saved);
}
let hooks = Rc::new(Hooks::default());
let leaving = cell.clone();
let saving = cell.clone();
self.entries.borrow_mut().insert(
key,
Entry {
kind,
epoch: self.epoch.get(),
state: Some(cell.clone() as Rc<dyn Any>),
hooks: hooks.clone(),
dispose: Some(Box::new(move || {
leaving.mounted.set(false);
if let Ok(mut state) = leaving.state.try_borrow_mut() {
state.dispose();
}
})),
save: Some(Saver {
name,
save: Box::new(move || saving.state.try_borrow().ok()?.save()),
}),
},
);
Mounted {
cell,
hooks,
previous: None,
}
}
}
struct Mounted<W: StatefulWidget> {
cell: Rc<StateCell<W::State>>,
hooks: Rc<Hooks>,
previous: Option<Rc<W>>,
}
fn escape(text: &str) -> String {
text.replace('\\', "\\\\")
.replace('\t', "\\t")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
fn unescape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('t') => out.push('\t'),
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some(other) => out.push(other),
None => out.push('\\'),
}
}
out
}
const ORDER: &str = "hooks were called in a different order than on the last build: call every \
hook unconditionally, in the same order, never inside an `if`, a loop or a \
closure";
#[derive(Default)]
struct Hooks {
slots: RefCell<Vec<Slot>>,
disposed: Cell<bool>,
}
enum Slot {
Value(Rc<dyn Any>),
Effect {
deps: Box<dyn Any>,
cleanup: Option<Cleanup>,
},
}
impl Hooks {
fn value<T: 'static>(&self, index: usize, init: impl FnOnce() -> T) -> Rc<T> {
let existing = self.slots.borrow().get(index).map(|slot| match slot {
Slot::Value(value) => Some(value.clone()),
Slot::Effect { .. } => None,
});
match existing {
Some(Some(value)) => value.downcast::<T>().unwrap_or_else(|_| panic!("{ORDER}")),
Some(None) => panic!("{ORDER}"),
None => {
let value = Rc::new(init());
let mut slots = self.slots.borrow_mut();
assert_eq!(slots.len(), index, "{ORDER}");
slots.push(Slot::Value(value.clone() as Rc<dyn Any>));
value
}
}
}
fn take_cleanup(&self, index: usize) -> Option<Cleanup> {
match self.slots.borrow_mut().get_mut(index) {
Some(Slot::Effect { cleanup, .. }) => cleanup.take(),
_ => None,
}
}
fn set_cleanup(&self, index: usize, cleanup: Option<Cleanup>) {
if let Some(Slot::Effect { cleanup: slot, .. }) = self.slots.borrow_mut().get_mut(index) {
*slot = cleanup;
}
}
fn dispose(&self) {
self.disposed.set(true);
let cleanups: Vec<Cleanup> = self
.slots
.borrow_mut()
.iter_mut()
.filter_map(|slot| match slot {
Slot::Effect { cleanup, .. } => cleanup.take(),
Slot::Value(_) => None,
})
.collect();
for cleanup in cleanups {
cleanup();
}
}
}
pub trait IntoCleanup {
fn into_cleanup(self) -> Option<Box<dyn FnOnce()>>;
}
impl IntoCleanup for () {
fn into_cleanup(self) -> Option<Cleanup> {
None
}
}
impl<F: FnOnce() + 'static> IntoCleanup for F {
fn into_cleanup(self) -> Option<Cleanup> {
Some(Box::new(self))
}
}
pub struct UseState<T> {
cell: Rc<RefCell<T>>,
}
impl<T> Clone for UseState<T> {
fn clone(&self) -> Self {
Self {
cell: self.cell.clone(),
}
}
}
impl<T: 'static> UseState<T> {
pub fn get(&self) -> T
where
T: Clone,
{
self.cell.borrow().clone()
}
pub fn with<R>(&self, read: impl FnOnce(&T) -> R) -> R {
read(&self.cell.borrow())
}
pub fn set(&self, value: T) {
*self.cell.borrow_mut() = value;
request_rebuild();
}
pub fn update(&self, change: impl FnOnce(&mut T)) {
change(&mut self.cell.borrow_mut());
request_rebuild();
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for UseState<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("UseState")
.field(&*self.cell.borrow())
.finish()
}
}
pub struct UseRef<T> {
cell: Rc<RefCell<T>>,
}
impl<T> Clone for UseRef<T> {
fn clone(&self) -> Self {
Self {
cell: self.cell.clone(),
}
}
}
impl<T: 'static> UseRef<T> {
pub fn get(&self) -> T
where
T: Clone,
{
self.cell.borrow().clone()
}
pub fn with<R>(&self, read: impl FnOnce(&T) -> R) -> R {
read(&self.cell.borrow())
}
pub fn set(&self, value: T) {
*self.cell.borrow_mut() = value;
}
pub fn update(&self, change: impl FnOnce(&mut T)) {
change(&mut self.cell.borrow_mut());
}
}
pub struct BuildContext<'a> {
key: StateKey,
kind: TypeId,
runtime: &'a Runtime,
theme: &'a Theme,
hooks: OnceCell<Rc<Hooks>>,
cursor: Cell<usize>,
intervals: Cell<usize>,
}
impl<'a> BuildContext<'a> {
fn new(key: StateKey, kind: TypeId, runtime: &'a Runtime, theme: &'a Theme) -> Self {
Self {
key,
kind,
runtime,
theme,
hooks: OnceCell::new(),
cursor: Cell::new(0),
intervals: Cell::new(0),
}
}
#[cfg(test)]
pub(crate) fn for_test(runtime: &'a Runtime, theme: &'a Theme) -> Self {
Self::new((WidgetId::ROOT, 0), TypeId::of::<()>(), runtime, theme)
}
pub fn theme(&self) -> &Theme {
self.theme
}
pub fn id(&self) -> WidgetId {
self.key.0
}
pub fn runtime(&self) -> &Runtime {
self.runtime
}
pub fn service<T: 'static>(&self) -> Option<Rc<T>> {
self.runtime.states.service::<T>()
}
fn hooks(&self) -> Rc<Hooks> {
self.hooks
.get_or_init(|| self.runtime.states.hooks_for(self.key, self.kind))
.clone()
}
fn next_slot(&self) -> usize {
let slot = self.cursor.get();
self.cursor.set(slot + 1);
slot
}
pub fn use_state<T: 'static>(&self, init: impl FnOnce() -> T) -> UseState<T> {
let cell = self
.hooks()
.value(self.next_slot(), || RefCell::new(init()));
UseState { cell }
}
pub fn use_ref<T: 'static>(&self, init: impl FnOnce() -> T) -> UseRef<T> {
let cell = self
.hooks()
.value(self.next_slot(), || RefCell::new(init()));
UseRef { cell }
}
pub fn use_memo<D: PartialEq + 'static, T: 'static>(
&self,
deps: D,
compute: impl FnOnce() -> T,
) -> Rc<T> {
let memo = self
.hooks()
.value(self.next_slot(), || RefCell::new(None::<(D, Rc<T>)>));
let mut memo = memo.borrow_mut();
match memo.as_ref() {
Some((previous, value)) if *previous == deps => value.clone(),
_ => {
let value = Rc::new(compute());
*memo = Some((deps, value.clone()));
value
}
}
}
pub fn use_interval(&self, period: std::time::Duration, on_tick: impl Into<Callback>) {
let index = self.intervals.get();
self.intervals.set(index + 1);
let id = {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
(self.key.0.as_u64(), self.key.1, index, period.as_millis()).hash(&mut hasher);
hasher.finish()
};
push_interval(Interval {
id,
period,
callback: on_tick.into(),
});
}
#[inline(never)]
pub fn block_back(&self, blocked: bool) {
if blocked {
BACK_BLOCKS.with(|blocks| blocks.set(blocks.get() + 1));
}
}
pub fn use_effect<D, C>(&self, deps: D, effect: impl FnOnce() -> C + 'static)
where
D: PartialEq + 'static,
C: IntoCleanup,
{
let hooks = self.hooks();
let index = self.next_slot();
let changed = {
let mut slots = hooks.slots.borrow_mut();
match slots.get_mut(index) {
None => {
assert_eq!(slots.len(), index, "{ORDER}");
slots.push(Slot::Effect {
deps: Box::new(deps),
cleanup: None,
});
true
}
Some(Slot::Effect { deps: old, .. }) => {
if old.downcast_ref::<D>().is_some_and(|old| *old == deps) {
false
} else {
*old = Box::new(deps);
true
}
}
Some(Slot::Value(_)) => panic!("{ORDER}"),
}
};
if changed {
self.runtime
.states
.effects
.borrow_mut()
.push(PendingEffect {
hooks,
index,
run: Box::new(move || effect().into_cleanup()),
});
}
}
}
pub trait StatelessWidget: 'static {
fn build(&self, cx: &BuildContext) -> Box<dyn Widget>;
fn into_widget(self) -> Component
where
Self: Sized,
{
Component::stateless(self)
}
}
impl<F> StatelessWidget for F
where
F: Fn(&BuildContext) -> Box<dyn Widget> + 'static,
{
fn build(&self, cx: &BuildContext) -> Box<dyn Widget> {
self(cx)
}
}
pub trait StatefulWidget: 'static {
type State: State<Widget = Self>;
fn create_state(&self) -> Self::State;
fn into_widget(self) -> Component
where
Self: Sized,
{
Component::stateful(self)
}
}
pub trait State: Sized + 'static {
type Widget: StatefulWidget<State = Self>;
fn init_state(&mut self, _cx: &StateContext<Self>) {}
fn did_update_widget(&mut self, _old: &Self::Widget, _cx: &StateContext<Self>) {}
fn dispose(&mut self) {}
fn save(&self) -> Option<String> {
None
}
fn restore(&mut self, _saved: &str) {}
fn build(&self, cx: &StateContext<Self>) -> Box<dyn Widget>;
}
pub(crate) struct StateCell<S: State> {
state: RefCell<S>,
widget: RefCell<Rc<S::Widget>>,
mounted: Cell<bool>,
}
pub struct StateHandle<S: State> {
cell: Rc<StateCell<S>>,
}
impl<S: State> Clone for StateHandle<S> {
fn clone(&self) -> Self {
Self {
cell: self.cell.clone(),
}
}
}
impl<S: State> StateHandle<S> {
pub fn set_state(&self, change: impl FnOnce(&mut S)) {
if !self.cell.mounted.get() {
return;
}
match self.cell.state.try_borrow_mut() {
Ok(mut state) => change(&mut state),
Err(_) => panic!(
"set_state was called while the state was being read — from inside `build`, or \
from inside another `set_state`. Change state from a handler, not while building"
),
}
request_rebuild();
}
pub fn read<R>(&self, read: impl FnOnce(&S) -> R) -> R {
read(&self.cell.state.borrow())
}
pub fn widget(&self) -> Rc<S::Widget> {
self.cell.widget.borrow().clone()
}
pub fn is_mounted(&self) -> bool {
self.cell.mounted.get()
}
pub fn callback(&self, change: impl Fn(&mut S) + 'static) -> Callback {
let handle = self.clone();
Callback::new(move || handle.set_state(|state| change(state)))
}
pub fn handler<T: Clone + 'static>(
&self,
change: impl Fn(&mut S, T) + 'static,
) -> impl Fn(T) -> Callback + 'static {
let handle = self.clone();
let change = Rc::new(change);
move |value| {
let (handle, change) = (handle.clone(), change.clone());
Callback::new(move || handle.set_state(|state| change(state, value.clone())))
}
}
}
pub struct StateContext<'c, 'a, S: State> {
cx: &'c BuildContext<'a>,
cell: &'c Rc<StateCell<S>>,
}
impl<'a, S: State> Deref for StateContext<'_, 'a, S> {
type Target = BuildContext<'a>;
fn deref(&self) -> &BuildContext<'a> {
self.cx
}
}
impl<S: State> StateContext<'_, '_, S> {
pub fn widget(&self) -> Rc<S::Widget> {
self.cell.widget.borrow().clone()
}
pub fn handle(&self) -> StateHandle<S> {
StateHandle {
cell: self.cell.clone(),
}
}
pub fn callback(&self, change: impl Fn(&mut S) + 'static) -> Callback {
self.handle().callback(change)
}
pub fn handler<T: Clone + 'static>(
&self,
change: impl Fn(&mut S, T) + 'static,
) -> impl Fn(T) -> Callback + 'static {
self.handle().handler(change)
}
}
type Builder<Msg> = Box<dyn FnOnce(&BuildContext) -> Box<dyn Widget<Msg>>>;
pub struct Component<Msg = Callback> {
inner: LazyChild<Msg>,
key: Option<u64>,
}
#[inline]
fn build_stateless<W: StatelessWidget>(widget: &W, cx: &BuildContext) -> Box<dyn Widget> {
#[cfg(feature = "hot-reload")]
{
subsecond::HotFn::current(<W as StatelessWidget>::build).call((widget, cx))
}
#[cfg(not(feature = "hot-reload"))]
{
widget.build(cx)
}
}
#[inline]
fn build_state<S: State>(state: &S, cx: &StateContext<S>) -> Box<dyn Widget> {
#[cfg(feature = "hot-reload")]
{
subsecond::HotFn::current(<S as State>::build).call((state, cx))
}
#[cfg(not(feature = "hot-reload"))]
{
state.build(cx)
}
}
impl Component<Callback> {
pub fn stateless<W: StatelessWidget>(widget: W) -> Self {
Self::from_builder(
TypeId::of::<W>(),
Box::new(move |cx: &BuildContext| build_stateless(&widget, cx)),
)
}
pub fn stateful<W: StatefulWidget>(widget: W) -> Self {
Self::from_builder(
TypeId::of::<W>(),
Box::new(move |cx: &BuildContext| {
let Mounted {
cell,
hooks,
previous,
} = cx.runtime.states.mount(cx.key, Rc::new(widget));
let _ = cx.hooks.set(hooks);
let state_cx = StateContext { cx, cell: &cell };
match previous {
None => cell.state.borrow_mut().init_state(&state_cx),
Some(old) => cell.state.borrow_mut().did_update_widget(&old, &state_cx),
}
let built = build_state(&*cell.state.borrow(), &state_cx);
built
}),
)
}
}
impl<Msg> Component<Msg> {
fn from_builder(kind: TypeId, build: Builder<Msg>) -> Self {
Self {
inner: LazyChild {
kind,
build: RefCell::new(Some(build)),
built: OnceCell::new(),
unbuilt: Unbuilt,
},
key: None,
}
}
pub fn with_key(mut self, key: impl Hash) -> Self {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
self.key = Some(hasher.finish());
self
}
fn restyle(&self, base: Style) -> Style {
base
}
}
struct LazyChild<Msg> {
kind: TypeId,
build: RefCell<Option<Builder<Msg>>>,
built: OnceCell<Box<dyn Widget<Msg>>>,
unbuilt: Unbuilt,
}
impl<Msg> LazyChild<Msg> {
fn expand(&self, id: WidgetId, runtime: &Runtime, theme: &Theme) {
if self.built.get().is_none() {
let build = self.build.borrow_mut().take();
if let Some(build) = build {
let cx = BuildContext::new((id, depth()), self.kind, runtime, theme);
let child = build(&cx);
let _ = self.built.set(child);
}
}
if let Some(child) = self.built.get() {
let _deeper = Deeper::enter();
child.expand(id, runtime, theme);
}
}
fn build_in(&self, id: WidgetId, runtime: &Runtime, theme: &Theme) {
if let Some(child) = self.built.get() {
child.build_in(id, runtime, theme);
}
}
fn build_themed(&self, theme: &Theme) {
if let Some(child) = self.built.get() {
child.build_themed(theme);
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
match self.built.get() {
Some(child) => child.children(),
None => {
#[cfg(debug_assertions)]
if self.build.borrow().is_some() {
panic!(
"a component's children were read before it was built: a traversal \
reached this tree before the walk that builds components did. Call \
`build_deferred(tree, &theme, &runtime)` on it first"
);
}
&[]
}
}
}
}
impl<Msg> Deref for LazyChild<Msg> {
type Target = dyn Widget<Msg>;
fn deref(&self) -> &(dyn Widget<Msg> + 'static) {
match self.built.get() {
Some(child) => child.as_ref(),
None => &self.unbuilt,
}
}
}
struct Unbuilt;
impl<Msg> Widget<Msg> for Unbuilt {
fn style(&self) -> Style {
Style::default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&[]
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn debug_name(&self) -> &'static str {
"Component"
}
}
crate::transparent::forward_transparent!(Component {
fn key(&self) -> Option<u64> {
self.key
}
fn positioned(&self) -> Option<crate::positioned::Positioning> {
self.inner.positioned()
}
fn theme_override(
&self,
inherited: &crate::theme::Theme,
) -> Option<Box<crate::theme::Theme>> {
self.inner.theme_override(inherited)
}
fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
self.inner.media_override(inherited)
}
fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
self.inner.scaffold_override()
}
fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
self.inner.scroll_behavior_override()
}
fn autofill_group(&self) -> bool {
self.inner.autofill_group()
}
fn area_toolbar(
&self,
context: crate::ToolbarContext,
) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
self.inner.area_toolbar(context)
}
});
pub fn component(build: impl Fn(&BuildContext) -> Box<dyn Widget> + 'static) -> Component {
Component::stateless(build)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{build_deferred, keyed, Container, Flex};
use frus_layout::Dimension;
type Builder = Rc<dyn Fn(&BuildContext) -> Box<dyn Widget>>;
type Log = Rc<RefCell<Vec<String>>>;
type Handle = Rc<RefCell<Option<StateHandle<LifeState>>>>;
fn log() -> Log {
Rc::new(RefCell::new(Vec::new()))
}
fn taken(log: &Log) -> Vec<String> {
std::mem::take(&mut *log.borrow_mut())
}
fn frame(runtime: &Runtime, root: &dyn Widget) {
runtime.states.begin_build();
build_deferred(root, &Theme::default(), runtime);
runtime.states.end_frame();
}
struct Life {
tag: u32,
log: Log,
out: Handle,
}
struct LifeState {
log: Log,
out: Handle,
count: i32,
}
impl StatefulWidget for Life {
type State = LifeState;
fn create_state(&self) -> LifeState {
LifeState {
log: self.log.clone(),
out: self.out.clone(),
count: 0,
}
}
}
impl State for LifeState {
type Widget = Life;
fn init_state(&mut self, cx: &StateContext<Self>) {
self.log
.borrow_mut()
.push(format!("init {}", cx.widget().tag));
*self.out.borrow_mut() = Some(cx.handle());
}
fn did_update_widget(&mut self, old: &Life, cx: &StateContext<Self>) {
self.log
.borrow_mut()
.push(format!("update {}->{}", old.tag, cx.widget().tag));
}
fn dispose(&mut self) {
self.log.borrow_mut().push("dispose".to_string());
}
fn build(&self, _cx: &StateContext<Self>) -> Box<dyn Widget> {
Box::new(Container::new().width(10.0 + self.count as f32).height(4.0))
}
}
fn life(tag: u32, log: &Log, out: &Handle) -> Component {
Life {
tag,
log: log.clone(),
out: out.clone(),
}
.into_widget()
}
fn handle() -> Handle {
Rc::new(RefCell::new(None))
}
fn width(widget: &dyn Widget) -> Dimension {
widget.style().width
}
struct Kept;
struct KeptState {
count: i32,
seen_at_init: Option<i32>,
}
impl StatefulWidget for Kept {
type State = KeptState;
fn create_state(&self) -> KeptState {
KeptState {
count: 0,
seen_at_init: None,
}
}
}
impl State for KeptState {
type Widget = Kept;
fn init_state(&mut self, _cx: &StateContext<Self>) {
self.seen_at_init = Some(self.count);
}
fn save(&self) -> Option<String> {
Some(format!("{}\tline two\n\\", self.count))
}
fn restore(&mut self, saved: &str) {
assert!(saved.ends_with("\tline two\n\\"), "{saved:?}");
self.count = saved.split('\t').next().unwrap().parse().unwrap();
}
fn build(&self, _cx: &StateContext<Self>) -> Box<dyn Widget> {
Box::new(Container::new().width(10.0 + self.count as f32).height(4.0))
}
}
#[test]
fn kept_states_cross_a_restart() {
let before = Runtime::default();
let (log, out) = (log(), handle());
let tree = || -> Box<dyn Widget> {
Box::new(crate::column![Kept.into_widget(), life(1, &log, &out)])
};
let root = tree();
frame(&before, root.as_ref());
let kept = before
.states
.entries
.borrow()
.values()
.find_map(|entry| entry.state.clone()?.downcast::<StateCell<KeptState>>().ok())
.unwrap();
kept.state.borrow_mut().count = 7;
let snapshot = before.states.snapshot();
assert_eq!(
snapshot.lines().count(),
1,
"only the state that saves: {snapshot}"
);
let after = Runtime::default();
after.states.restore_from(&snapshot);
let root = tree();
frame(&after, root.as_ref());
let restored = after
.states
.entries
.borrow()
.values()
.find_map(|entry| entry.state.clone()?.downcast::<StateCell<KeptState>>().ok())
.unwrap();
let state = restored.state.borrow();
assert_eq!(state.count, 7);
assert_eq!(state.seen_at_init, Some(7), "restored before init_state");
assert!(after.states.restoring.borrow().is_empty(), "taken once");
}
#[test]
fn saved_text_survives_escaping() {
for text in [
"plain",
"tab\there",
"two\nlines",
"back\\slash",
"\\n literally",
"",
] {
assert_eq!(unescape(&escape(text)), text);
assert!(!escape(text).contains(['\n', '\t']));
}
}
#[test]
fn a_stateless_component_is_the_widget_it_builds() {
let runtime = Runtime::default();
let root = component(|_| Box::new(Container::new().width(120.0).height(30.0)));
frame(&runtime, &root);
assert_eq!(width(&root), Dimension::Length(120.0));
assert_eq!(root.style().height, Dimension::Length(30.0));
}
#[test]
fn a_state_is_made_once_and_survives_rebuilds() {
let (runtime, log, out) = (Runtime::default(), log(), handle());
let first = life(1, &log, &out);
frame(&runtime, &first);
assert_eq!(taken(&log), ["init 1"]);
assert_eq!(width(&first), Dimension::Length(10.0));
out.borrow().as_ref().unwrap().set_state(|s| s.count = 5);
assert!(take_rebuild_request(), "set_state asks for a rebuild");
let second = life(2, &log, &out);
frame(&runtime, &second);
assert_eq!(
taken(&log),
["update 1->2"],
"the same place, the same kind: the state is kept and told what changed"
);
assert_eq!(
width(&second),
Dimension::Length(15.0),
"and it is the kept one"
);
}
#[test]
fn a_handler_carries_the_value_a_widget_reports_into_the_state() {
let (runtime, log, out) = (Runtime::default(), log(), handle());
frame(&runtime, &life(1, &log, &out));
let handle = out.borrow().clone().unwrap();
let on_value = handle.handler(|s, by: i32| s.count += by);
let (three, four) = (on_value(3), on_value(4));
assert_eq!(
handle.read(|s| s.count),
0,
"nothing changes until it is delivered"
);
three.call();
four.call();
four.call();
assert_eq!(
handle.read(|s| s.count),
11,
"each delivery makes the change"
);
assert!(take_rebuild_request(), "and it asks for the rebuild");
}
#[test]
fn a_component_that_blocks_back_does_so_for_one_build_only() {
let runtime = Runtime::default();
let open = Rc::new(Cell::new(true));
let asked = open.clone();
let root = component(move |cx| {
cx.block_back(asked.get());
Box::new(Container::new().width(4.0).height(4.0))
});
frame(&runtime, &root);
assert!(back_blocked(), "an open menu is in the way");
open.set(false);
frame(&runtime, &root);
assert!(!back_blocked(), "each build says it afresh");
}
#[test]
fn a_state_is_disposed_when_a_rebuild_no_longer_reaches_it() {
let (runtime, log, out) = (Runtime::default(), log(), handle());
frame(&runtime, &Flex::column().child(life(1, &log, &out)));
taken(&log);
assert_eq!(runtime.states.len(), 1);
frame(&runtime, &Flex::column().child(Container::new()));
assert_eq!(taken(&log), ["dispose"]);
assert!(runtime.states.is_empty());
let handle = out.borrow().clone().unwrap();
assert!(!handle.is_mounted());
take_rebuild_request();
handle.set_state(|s| s.count = 9);
assert!(
!take_rebuild_request(),
"a state that has left cannot ask for a rebuild"
);
}
#[test]
fn a_different_kind_of_widget_in_the_same_place_starts_over() {
let (runtime, log, out) = (Runtime::default(), log(), handle());
frame(&runtime, &Flex::column().child(life(1, &log, &out)));
taken(&log);
let other = component(|cx| {
cx.use_state(|| 0u8);
Box::new(Container::new())
});
frame(&runtime, &Flex::column().child(other));
assert_eq!(
taken(&log),
["dispose"],
"the old kind is disposed, not reused"
);
}
#[test]
fn a_key_carries_the_state_wherever_the_siblings_go() {
let (runtime, log) = (Runtime::default(), log());
let (a, b) = (handle(), handle());
let column = |order: [u32; 2]| {
let mut column = Flex::column();
for tag in order {
let out = if tag == 1 { &a } else { &b };
column = column.child(keyed(tag, life(tag, &log, out)));
}
column
};
frame(&runtime, &column([1, 2]));
a.borrow().as_ref().unwrap().set_state(|s| s.count = 1);
b.borrow().as_ref().unwrap().set_state(|s| s.count = 2);
taken(&log);
frame(&runtime, &column([2, 1]));
let counts = |h: &Handle| h.borrow().as_ref().unwrap().read(|s| s.count);
assert_eq!(
(counts(&a), counts(&b)),
(1, 2),
"each state stayed with its key"
);
assert_eq!(runtime.states.len(), 2);
assert!(
!taken(&log)
.iter()
.any(|line| line == "dispose" || line.starts_with("init")),
"and neither was made again or let go"
);
}
#[test]
fn a_component_that_builds_a_component_keeps_both_states() {
let (runtime, log, out) = (Runtime::default(), log(), handle());
let (inner_log, inner_out) = (log.clone(), out.clone());
let outer = component(move |cx| {
cx.use_state(|| 0);
Box::new(life(7, &inner_log, &inner_out))
});
frame(&runtime, &outer);
assert_eq!(
runtime.states.len(),
2,
"one for the hooks, one for the state"
);
assert_eq!(taken(&log), ["init 7"]);
}
#[test]
fn hooks_keep_their_values_between_builds() {
let runtime = Runtime::default();
let seen = Rc::new(RefCell::new(Vec::new()));
let build = {
let seen = seen.clone();
Rc::new(move |cx: &BuildContext| -> Box<dyn Widget> {
let taps = cx.use_state(|| 10);
let scratch = cx.use_ref(|| 0);
scratch.update(|n| *n += 1);
taps.update(|n| *n += 1);
seen.borrow_mut().push((taps.get(), scratch.get()));
Box::new(Container::new())
})
};
let make = |build: &Builder| {
let build = build.clone();
Component::stateless(move |cx: &BuildContext| build(cx))
};
let build: Builder = build;
frame(&runtime, &make(&build));
assert!(
take_rebuild_request(),
"changing a state hook asks for a rebuild"
);
frame(&runtime, &make(&build));
frame(&runtime, &make(&build));
assert_eq!(*seen.borrow(), [(11, 1), (12, 2), (13, 3)]);
}
#[test]
fn use_memo_recomputes_only_when_its_dependencies_change() {
let runtime = Runtime::default();
let computed = Rc::new(Cell::new(0));
let dep = Rc::new(Cell::new(1));
let build: Builder = {
let (computed, dep) = (computed.clone(), dep.clone());
Rc::new(move |cx| {
let d = dep.get();
let value = cx.use_memo(d, || {
computed.set(computed.get() + 1);
d * 100
});
Box::new(Container::new().width(*value as f32))
})
};
let make = || {
let build = build.clone();
Component::stateless(move |cx: &BuildContext| build(cx))
};
let root = make();
frame(&runtime, &root);
assert_eq!(
(computed.get(), width(&root)),
(1, Dimension::Length(100.0))
);
let root = make();
frame(&runtime, &root);
assert_eq!(computed.get(), 1, "the same dependency: not computed again");
dep.set(2);
let root = make();
frame(&runtime, &root);
assert_eq!(
(computed.get(), width(&root)),
(2, Dimension::Length(200.0))
);
}
#[test]
fn effects_run_after_the_build_and_clean_up_before_the_next_and_at_the_end() {
let (runtime, log) = (Runtime::default(), log());
let dep = Rc::new(Cell::new(1));
let build: Builder = {
let (log, dep) = (log.clone(), dep.clone());
Rc::new(move |cx| {
let d = dep.get();
let log = log.clone();
log.borrow_mut().push(format!("build {d}"));
cx.use_effect(d, move || {
log.borrow_mut().push(format!("effect {d}"));
move || log.borrow_mut().push(format!("cleanup {d}"))
});
Box::new(Container::new())
})
};
let make = || {
let build = build.clone();
Component::stateless(move |cx: &BuildContext| build(cx))
};
frame(&runtime, &make());
assert_eq!(
taken(&log),
["build 1", "effect 1"],
"after the build, not during it"
);
frame(&runtime, &make());
assert_eq!(
taken(&log),
["build 1"],
"the same dependency: nothing runs"
);
dep.set(2);
frame(&runtime, &make());
assert_eq!(
taken(&log),
["build 2", "cleanup 1", "effect 2"],
"the old one is undone before the new one starts"
);
frame(&runtime, &Flex::column().child(Container::new()));
assert_eq!(taken(&log), ["cleanup 2"]);
}
#[test]
#[should_panic(expected = "different order")]
fn hooks_called_in_a_different_order_are_caught() {
let runtime = Runtime::default();
let first = Rc::new(Cell::new(true));
let build: Builder = {
let first = first.clone();
Rc::new(move |cx| {
if first.get() {
cx.use_state(|| 1i32);
}
cx.use_ref(String::new);
Box::new(Container::new())
})
};
let make = || {
let build = build.clone();
Component::stateless(move |cx: &BuildContext| build(cx))
};
frame(&runtime, &make());
first.set(false);
frame(&runtime, &make());
}
#[test]
#[should_panic(expected = "set_state was called while")]
fn changing_state_from_inside_build_is_refused() {
struct Greedy;
struct GreedyState;
impl StatefulWidget for Greedy {
type State = GreedyState;
fn create_state(&self) -> GreedyState {
GreedyState
}
}
impl State for GreedyState {
type Widget = Greedy;
fn build(&self, cx: &StateContext<Self>) -> Box<dyn Widget> {
cx.handle().set_state(|_| {});
Box::new(Container::new())
}
}
frame(&Runtime::default(), &Greedy.into_widget());
}
#[test]
fn an_interval_is_asked_for_by_each_build_that_wants_it() {
let runtime = Runtime::default();
let on = Rc::new(Cell::new(true));
let build: Builder = {
let on = on.clone();
Rc::new(move |cx| {
if on.get() {
cx.use_interval(std::time::Duration::from_secs(1), || {});
cx.use_interval(std::time::Duration::from_secs(5), || {});
}
Box::new(Container::new())
})
};
let make = || {
let build = build.clone();
Component::stateless(move |cx: &BuildContext| build(cx))
};
frame(&runtime, &make());
let first = intervals();
assert_eq!(first.len(), 2);
assert_ne!(first[0].id, first[1].id, "two timers are told apart");
frame(&runtime, &make());
let second = intervals();
assert_eq!(
first.iter().map(|i| i.id).collect::<Vec<_>>(),
second.iter().map(|i| i.id).collect::<Vec<_>>(),
"and each is the same timer from one build to the next"
);
on.set(false);
frame(&runtime, &make());
assert!(
intervals().is_empty(),
"a build that stops asking stops the timer"
);
}
#[test]
fn a_service_provided_to_the_runtime_is_reachable_from_a_build() {
let runtime = Runtime::default();
runtime.states.provide(Rc::new(41u32));
let root = component(|cx| {
let n = cx.service::<u32>().expect("provided");
Box::new(Container::new().width(*n as f32 + 1.0))
});
frame(&runtime, &root);
assert_eq!(width(&root), Dimension::Length(42.0));
assert!(runtime.states.service::<String>().is_none());
}
}